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:
Dan Albert 2020-10-09 21:25:56 -07:00
parent d414c00b74
commit 974b6590d8
14 changed files with 497 additions and 322 deletions

View File

@ -57,14 +57,14 @@ from dcs.task import (
) )
from dcs.terrain.terrain import Airport, NoParkingSlotError from dcs.terrain.terrain import Airport, NoParkingSlotError
from dcs.translation import String 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.unitgroup import FlyingGroup, Group, ShipGroup, StaticGroup
from dcs.unittype import FlyingType, UnitType from dcs.unittype import FlyingType, UnitType
from game import db from game import db
from game.data.cap_capabilities_db import GUNFIGHTERS from game.data.cap_capabilities_db import GUNFIGHTERS
from game.settings import Settings 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.airfields import RunwayData
from gen.airsupportgen import AirSupport from gen.airsupportgen import AirSupport
from gen.ato import AirTaskingOrder, Package from gen.ato import AirTaskingOrder, Package
@ -76,9 +76,10 @@ from gen.flights.flight import (
FlightWaypointType, FlightWaypointType,
) )
from gen.radios import MHz, Radio, RadioFrequency, RadioRegistry, get_radio 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 theater.controlpoint import ControlPoint, ControlPointType
from .conflictgen import Conflict from .conflictgen import Conflict
from .flights.traveltime import PackageWaypointTiming, TotEstimator
from .naming import namegen from .naming import namegen
WARM_START_HELI_AIRSPEED = 120 WARM_START_HELI_AIRSPEED = 120
@ -86,8 +87,6 @@ WARM_START_HELI_ALT = 500
WARM_START_ALTITUDE = 3000 WARM_START_ALTITUDE = 3000
WARM_START_AIRSPEED = 550 WARM_START_AIRSPEED = 550
CAP_DURATION = 30 # minutes
RTB_ALTITUDE = 800 RTB_ALTITUDE = 800
RTB_DISTANCE = 5000 RTB_DISTANCE = 5000
HELI_ALT = 500 HELI_ALT = 500
@ -217,7 +216,7 @@ class FlightData:
#: True if this flight belongs to the player's coalition. #: True if this flight belongs to the player's coalition.
friendly: bool 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 departure_delay: int
#: Arrival airport. #: Arrival airport.
@ -533,148 +532,6 @@ AIRCRAFT_DATA["P-51D-30-NA"] = AIRCRAFT_DATA["P-51D"]
AIRCRAFT_DATA["P-47D-30"] = 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: class AircraftConflictGenerator:
def __init__(self, mission: Mission, conflict: Conflict, settings: Settings, def __init__(self, mission: Mission, conflict: Conflict, settings: Settings,
game, radio_registry: RadioRegistry): game, radio_registry: RadioRegistry):
@ -702,8 +559,13 @@ class AircraftConflictGenerator:
except KeyError: except KeyError:
return get_fallback_channel(airframe) return get_fallback_channel(airframe)
def _start_type(self) -> StartType: @staticmethod
return self.settings.cold_start and StartType.Cold or StartType.Warm 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], def _setup_group(self, group: FlyingGroup, for_task: Type[Task],
flight: Flight, dynamic_runways: Dict[str, RunwayData]): flight: Flight, dynamic_runways: Dict[str, RunwayData]):
@ -808,15 +670,10 @@ class AircraftConflictGenerator:
return runways[0] return runways[0]
def _generate_at_airport(self, name: str, side: Country, def _generate_at_airport(self, name: str, side: Country,
unit_type: FlyingType, count: int, unit_type: FlyingType, count: int, start_type: str,
client_count: int, airport: Optional[Airport] = None) -> FlyingGroup:
airport: Optional[Airport] = None,
start_type=None) -> FlyingGroup:
assert count > 0 assert count > 0
if start_type is None:
start_type = self._start_type()
logging.info("airgen: {} for {} at {}".format(unit_type, side.id, airport)) logging.info("airgen: {} for {} at {}".format(unit_type, side.id, airport))
return self.m.flight_group_from_airport( return self.m.flight_group_from_airport(
country=side, country=side,
@ -824,11 +681,11 @@ class AircraftConflictGenerator:
aircraft_type=unit_type, aircraft_type=unit_type,
airport=airport, airport=airport,
maintask=None, maintask=None,
start_type=start_type, start_type=self._start_type(start_type),
group_size=count, group_size=count,
parking_slots=None) 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 assert count > 0
if unit_type in helicopters.helicopter_map.values(): if unit_type in helicopters.helicopter_map.values():
@ -850,21 +707,16 @@ class AircraftConflictGenerator:
altitude=alt, altitude=alt,
speed=speed, speed=speed,
maintask=None, maintask=None,
start_type=self._start_type(),
group_size=count) group_size=count)
group.points[0].alt_type = "RADIO" group.points[0].alt_type = "RADIO"
return group return group
def _generate_at_group(self, name: str, side: Country, def _generate_at_group(self, name: str, side: Country,
unit_type: FlyingType, count: int, client_count: int, unit_type: FlyingType, count: int, start_type: str,
at: Union[ShipGroup, StaticGroup], at: Union[ShipGroup, StaticGroup]) -> FlyingGroup:
start_type=None) -> FlyingGroup:
assert count > 0 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)) logging.info("airgen: {} for {} at unit {}".format(unit_type, side.id, at))
return self.m.flight_group_from_unit( return self.m.flight_group_from_unit(
country=side, country=side,
@ -872,34 +724,9 @@ class AircraftConflictGenerator:
aircraft_type=unit_type, aircraft_type=unit_type,
pad_group=at, pad_group=at,
maintask=None, maintask=None,
start_type=start_type, start_type=self._start_type(start_type),
group_size=count) 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): def _add_radio_waypoint(self, group: FlyingGroup, position, altitude: int, airspeed: int = 600):
point = group.add_waypoint(position, altitude, airspeed) point = group.add_waypoint(position, altitude, airspeed)
point.alt_type = "RADIO" point.alt_type = "RADIO"
@ -970,110 +797,91 @@ class AircraftConflictGenerator:
logging.info(f"Generating flight: {flight.unit_type}") logging.info(f"Generating flight: {flight.unit_type}")
group = self.generate_planned_flight(flight.from_cp, country, group = self.generate_planned_flight(flight.from_cp, country,
flight) flight)
self.setup_flight_group(group, flight, timing, dynamic_runways) self.setup_flight_group(group, package, flight, timing,
self.setup_group_activation_trigger(flight, group) dynamic_runways)
def setup_group_activation_trigger(self, flight, group): def set_activation_time(self, flight: Flight, group: FlyingGroup,
if flight.scheduled_in > 0 and flight.client_count == 0: delay: int) -> None:
# Note: Late activation causes the waypoint TOTs to look *weird* in the
if flight.start_type != "In Flight" and flight.from_cp.cptype not in [ControlPointType.AIRCRAFT_CARRIER_GROUP, ControlPointType.LHA_GROUP]: # mission editor. Waypoint times will be relative to the group
group.late_activation = False # activation time rather than in absolute local time. A flight delayed
group.uncontrolled = True # 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.
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))
if flight.flight_type == FlightType.INTERCEPTION:
self.setup_interceptor_triggers(group, 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)
else:
group.late_activation = True group.late_activation = True
activation_trigger = TriggerOnce(Event.NoEvent, "FlightLateActivationTrigger" + str(group.id))
activation_trigger.add_condition(TimeAfter(seconds=flight.scheduled_in*60))
if(flight.from_cp.cptype == ControlPointType.AIRBASE): activation_trigger = TriggerOnce(
if flight.from_cp.captured: Event.NoEvent, f"FlightLateActivationTrigger{group.id}")
activation_trigger.add_condition(CoalitionHasAirdrome(self.game.get_player_coalition_id(), flight.from_cp.id)) activation_trigger.add_condition(TimeAfter(seconds=delay))
else:
activation_trigger.add_condition(CoalitionHasAirdrome(self.game.get_enemy_coalition_id(), flight.from_cp.id))
if flight.flight_type == FlightType.INTERCEPTION:
self.setup_interceptor_triggers(group, flight, activation_trigger)
self.prevent_spawn_at_hostile_airbase(flight, activation_trigger)
activation_trigger.add_action(ActivateGroup(group.id)) activation_trigger.add_action(ActivateGroup(group.id))
self.m.triggerrules.triggers.append(activation_trigger) self.m.triggerrules.triggers.append(activation_trigger)
def setup_interceptor_triggers(self, 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
activation_trigger = TriggerOnce(Event.NoEvent,
f"FlightStartTrigger{group.id}")
activation_trigger.add_condition(TimeAfter(seconds=delay))
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)
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
detection_zone = self.m.triggers.add_triggerzone(flight.from_cp.position, radius=25000, hidden=False, name="ITZ")
if flight.from_cp.captured: 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 coalition = self.game.get_player_coalition_id()
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))
else: else:
activation_trigger.add_condition(PartOfCoalitionInZone(self.game.get_player_color(), detection_zone.id)) coalition = self.game.get_enemy_coalition_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))
trigger.add_condition(
CoalitionHasAirdrome(coalition, flight.from_cp.id))
def generate_planned_flight(self, cp, country, flight:Flight): def generate_planned_flight(self, cp, country, flight:Flight):
try: try:
if flight.client_count == 0 and self.game.settings.perf_ai_parking_start:
flight.start_type = "Cold"
if flight.start_type == "In Flight": 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), name=namegen.next_unit_name(country, cp.id, flight.unit_type),
side=country, side=country,
unit_type=flight.unit_type, unit_type=flight.unit_type,
count=flight.count, count=flight.count,
client_count=0,
at=cp.position) at=cp.position)
else: elif cp.is_fleet:
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_name = cp.get_carrier_group_name()
group = self._generate_at_group( group = self._generate_at_group(
name=namegen.next_unit_name(country, cp.id, flight.unit_type), name=namegen.next_unit_name(country, cp.id, flight.unit_type),
side=country, side=country,
unit_type=flight.unit_type, unit_type=flight.unit_type,
count=flight.count, count=flight.count,
client_count=0, start_type=flight.start_type,
at=self.m.find_group(group_name), at=self.m.find_group(group_name))
start_type=st)
else: else:
group = self._generate_at_airport( group = self._generate_at_airport(
name=namegen.next_unit_name(country, cp.id, flight.unit_type), name=namegen.next_unit_name(country, cp.id, flight.unit_type),
side=country, side=country,
unit_type=flight.unit_type, unit_type=flight.unit_type,
count=flight.count, count=flight.count,
client_count=0, start_type=flight.start_type,
airport=cp.airport, airport=cp.airport)
start_type=st)
except Exception as e: except Exception as e:
# Generated when there is no place on Runway or on Parking Slots # Generated when there is no place on Runway or on Parking Slots
logging.error(e) logging.error(e)
logging.warning("No room on runway or parking slots. Starting from the air.") logging.warning("No room on runway or parking slots. Starting from the air.")
flight.start_type = "In Flight" flight.start_type = "In Flight"
group = self._generate_group( group = self._generate_inflight(
name=namegen.next_unit_name(country, cp.id, flight.unit_type), name=namegen.next_unit_name(country, cp.id, flight.unit_type),
side=country, side=country,
unit_type=flight.unit_type, unit_type=flight.unit_type,
count=flight.count, count=flight.count,
client_count=0,
at=cp.position) at=cp.position)
group.points[0].alt = 1500 group.points[0].alt = 1500
@ -1179,8 +987,8 @@ class AircraftConflictGenerator:
logging.error(f"Unhandled flight type: {flight.flight_type.name}") logging.error(f"Unhandled flight type: {flight.flight_type.name}")
self.configure_behavior(group) self.configure_behavior(group)
def setup_flight_group(self, group: FlyingGroup, flight: Flight, def setup_flight_group(self, group: FlyingGroup, package: Package,
timing: PackageWaypointTiming, flight: Flight, timing: PackageWaypointTiming,
dynamic_runways: Dict[str, RunwayData]) -> None: dynamic_runways: Dict[str, RunwayData]) -> None:
flight_type = flight.flight_type flight_type = flight.flight_type
if flight_type in [FlightType.CAP, FlightType.BARCAP, FlightType.TARCAP, if flight_type in [FlightType.CAP, FlightType.BARCAP, FlightType.TARCAP,
@ -1204,6 +1012,9 @@ class AircraftConflictGenerator:
for waypoint in flight.points: for waypoint in flight.points:
waypoint.tot = None 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: for point in flight.points:
if point.only_for_player and not flight.client_count: if point.only_for_player and not flight.client_count:
continue continue
@ -1214,9 +1025,53 @@ class AircraftConflictGenerator:
# Set here rather than when the FlightData is created so they waypoints # Set here rather than when the FlightData is created so they waypoints
# have their TOTs set. # have their TOTs set.
self.flights[-1].waypoints = flight.points self.flights[-1].waypoints = [takeoff_point] + flight.points
self._setup_custom_payload(flight, group) 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: class PydcsWaypointBuilder:
def __init__(self, waypoint: FlightWaypoint, group: FlyingGroup, def __init__(self, waypoint: FlightWaypoint, group: FlyingGroup,
@ -1243,10 +1098,8 @@ class PydcsWaypointBuilder:
waypoint.speed_locked = False waypoint.speed_locked = False
@classmethod @classmethod
def for_waypoint(cls, waypoint: FlightWaypoint, def for_waypoint(cls, waypoint: FlightWaypoint, group: FlyingGroup,
group: FlyingGroup, flight: Flight, timing: PackageWaypointTiming,
flight: Flight,
timing: PackageWaypointTiming,
mission: Mission) -> PydcsWaypointBuilder: mission: Mission) -> PydcsWaypointBuilder:
builders = { builders = {
FlightWaypointType.EGRESS: EgressPointBuilder, FlightWaypointType.EGRESS: EgressPointBuilder,
@ -1391,9 +1244,7 @@ class RaceTrackBuilder(PydcsWaypointBuilder):
pattern=OrbitAction.OrbitPattern.RaceTrack pattern=OrbitAction.OrbitPattern.RaceTrack
)) ))
start = self.timing.race_track_start self.set_waypoint_tot(waypoint, self.timing.race_track_start)
if start is not None:
self.set_waypoint_tot(waypoint, start)
racetrack.stop_after_time(self.timing.race_track_end) racetrack.stop_after_time(self.timing.race_track_end)
waypoint.add_task(racetrack) waypoint.add_task(racetrack)
return waypoint return waypoint

View File

@ -52,7 +52,7 @@ class Package:
delay: int = field(default=0) delay: int = field(default=0)
#: Desired TOT measured in seconds from mission start. #: 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) waypoints: Optional[PackageWaypoints] = field(default=None)

View File

@ -1,3 +1,4 @@
import datetime
import os import os
from collections import defaultdict from collections import defaultdict
from dataclasses import dataclass from dataclasses import dataclass
@ -116,9 +117,10 @@ class BriefingGenerator(MissionInfoGenerator):
assert not flight.client_units assert not flight.client_units
aircraft = flight.aircraft_type aircraft = flight.aircraft_type
flight_unit_name = db.unit_type_name(aircraft) flight_unit_name = db.unit_type_name(aircraft)
delay = datetime.timedelta(seconds=flight.departure_delay)
self.description += ( self.description += (
f"{flight.flight_type.name} {flight_unit_name} x {flight.size}, " 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): def generate(self):

View File

@ -33,6 +33,7 @@ from gen.flights.flight import (
FlightType, FlightType,
) )
from gen.flights.flightplan import FlightPlanBuilder from gen.flights.flightplan import FlightPlanBuilder
from gen.flights.traveltime import TotEstimator
from theater import ( from theater import (
ControlPoint, ControlPoint,
FrontLine, FrontLine,
@ -185,11 +186,13 @@ class PackageBuilder:
def __init__(self, location: MissionTarget, def __init__(self, location: MissionTarget,
closest_airfields: ClosestAirfields, closest_airfields: ClosestAirfields,
global_inventory: GlobalAircraftInventory, global_inventory: GlobalAircraftInventory,
is_player: bool) -> None: is_player: bool,
start_type: str) -> None:
self.package = Package(location) self.package = Package(location)
self.allocator = AircraftAllocator(closest_airfields, global_inventory, self.allocator = AircraftAllocator(closest_airfields, global_inventory,
is_player) is_player)
self.global_inventory = global_inventory self.global_inventory = global_inventory
self.start_type = start_type
def plan_flight(self, plan: ProposedFlight) -> bool: def plan_flight(self, plan: ProposedFlight) -> bool:
"""Allocates aircraft for the given flight and adds them to the package. """Allocates aircraft for the given flight and adds them to the package.
@ -203,7 +206,8 @@ class PackageBuilder:
if assignment is None: if assignment is None:
return False return False
airfield, aircraft = assignment 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) self.package.add_flight(flight)
return True return True
@ -444,11 +448,18 @@ class CoalitionMissionPlanner:
def plan_mission(self, mission: ProposedMission) -> None: def plan_mission(self, mission: ProposedMission) -> None:
"""Allocates aircraft for a proposed mission and adds it to the ATO.""" """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( builder = PackageBuilder(
mission.location, mission.location,
self.objective_finder.closest_airfields_to(mission.location), self.objective_finder.closest_airfields_to(mission.location),
self.game.aircraft_inventory, self.game.aircraft_inventory,
self.is_player self.is_player,
start_type
) )
missing_types: Set[FlightType] = set() missing_types: Set[FlightType] = set()
@ -497,16 +508,18 @@ class CoalitionMissionPlanner:
margin=5 margin=5
) )
for package in self.ato.packages: for package in self.ato.packages:
tot = TotEstimator(package).earliest_tot()
if package.primary_task in dca_types: if package.primary_task in dca_types:
# All CAP missions should be on station in 15-25 minutes. # All CAP missions should be on station ASAP.
package.time_over_target = (20 + random.randint(-5, 5)) * 60 package.time_over_target = tot
else: else:
# But other packages should be spread out a bit. # But other packages should be spread out a bit. Note that take
package.delay = next(start_time) # times are delayed, but all aircraft will become active at
# TODO: Compute TOT based on package. # mission start. This makes it more worthwhile to attack enemy
package.time_over_target = (package.delay + 40) * 60 # airfields to hit grounded aircraft, since they're more likely
for flight in package.flights: # to be present. Runway and air started aircraft will be
flight.scheduled_in = package.delay # delayed until their takeoff time by AirConflictGenerator.
package.time_over_target = next(start_time) * 60 + tot
def message(self, title, text) -> None: def message(self, title, text) -> None:
"""Emits a planning message to the player. """Emits a planning message to the player.

View File

@ -97,7 +97,6 @@ class FlightWaypoint:
# flight's offset in the UI. # flight's offset in the UI.
self.tot: Optional[int] = None self.tot: Optional[int] = None
@classmethod @classmethod
def from_pydcs(cls, point: MovingPoint, def from_pydcs(cls, point: MovingPoint,
from_cp: ControlPoint) -> "FlightWaypoint": from_cp: ControlPoint) -> "FlightWaypoint":
@ -130,13 +129,10 @@ class Flight:
client_count: int = 0 client_count: int = 0
use_custom_loadout = False use_custom_loadout = False
preset_loadout_name = "" preset_loadout_name = ""
start_type = "Runway"
group = False # Contains DCS Mission group data after mission has been generated group = False # Contains DCS Mission group data after mission has been generated
# How long before this flight should take off def __init__(self, unit_type: UnitType, count: int, from_cp: ControlPoint,
scheduled_in = 0 flight_type: FlightType, start_type: str) -> None:
def __init__(self, unit_type: UnitType, count: int, from_cp: ControlPoint, flight_type: FlightType):
self.unit_type = unit_type self.unit_type = unit_type
self.count = count self.count = count
self.from_cp = from_cp self.from_cp = from_cp
@ -144,11 +140,16 @@ class Flight:
self.points: List[FlightWaypoint] = [] self.points: List[FlightWaypoint] = []
self.targets: List[MissionTarget] = [] self.targets: List[MissionTarget] = []
self.loadout: Dict[str, str] = {} 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): def __repr__(self):
return self.flight_type.name + " | " + str(self.count) + "x" + db.unit_type_name(self.unit_type) \ 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 # Test
@ -157,6 +158,6 @@ if __name__ == '__main__':
from theater import ControlPoint, Point, List from theater import ControlPoint, Point, List
from_cp = ControlPoint(0, "AA", Point(0, 0), Point(0, 0), [], 0, 0) 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 f.scheduled_in = 50
print(f) print(f)

285
gen/flights/traveltime.py Normal file
View 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)

View File

@ -1,7 +1,6 @@
"""Qt data models for game objects.""" """Qt data models for game objects."""
import datetime import datetime
from enum import auto, IntEnum from typing import Any, Callable, Dict, Iterator, Optional, TypeVar
from typing import Any, Callable, Dict, Iterator, TypeVar, Optional
from PySide2.QtCore import ( from PySide2.QtCore import (
QAbstractListModel, QAbstractListModel,
@ -15,6 +14,7 @@ from game import db
from game.game import Game from game.game import Game
from gen.ato import AirTaskingOrder, Package from gen.ato import AirTaskingOrder, Package
from gen.flights.flight import Flight from gen.flights.flight import Flight
from gen.flights.traveltime import TotEstimator
from qt_ui.uiconstants import AIRCRAFT_ICONS from qt_ui.uiconstants import AIRCRAFT_ICONS
from theater.missiontarget import MissionTarget from theater.missiontarget import MissionTarget
@ -119,15 +119,15 @@ class PackageModel(QAbstractListModel):
return flight return flight
return None return None
@staticmethod def text_for_flight(self, flight: Flight) -> str:
def text_for_flight(flight: Flight) -> str:
"""Returns the text that should be displayed for the flight.""" """Returns the text that should be displayed for the flight."""
task = flight.flight_type.name task = flight.flight_type.name
count = flight.count count = flight.count
name = db.unit_type_name(flight.unit_type) name = db.unit_type_name(flight.unit_type)
delay = flight.scheduled_in estimator = TotEstimator(self.package)
delay = datetime.timedelta(seconds=estimator.mission_start_time(flight))
origin = flight.from_cp.name origin = flight.from_cp.name
return f"[{task}] {count} x {name} from {origin} in {delay} minutes" return f"[{task}] {count} x {name} from {origin} in {delay}"
@staticmethod @staticmethod
def icon_for_flight(flight: Flight) -> Optional[QIcon]: def icon_for_flight(flight: Flight) -> Optional[QIcon]:

View File

@ -147,6 +147,8 @@ class QTopPanel(QFrame):
if not self.ato_has_clients() and not self.confirm_no_client_launch(): if not self.ato_has_clients() and not self.confirm_no_client_launch():
return return
# TODO: Verify no negative start times.
# TODO: Refactor this nonsense. # TODO: Refactor this nonsense.
game_event = None game_event = None
for event in self.game.events: for event in self.game.events:

View File

@ -24,6 +24,7 @@ from PySide2.QtWidgets import (
from game import db from game import db
from gen.ato import Package from gen.ato import Package
from gen.flights.flight import Flight from gen.flights.flight import Flight
from gen.flights.traveltime import TotEstimator
from qt_ui.windows.GameUpdateSignal import GameUpdateSignal from qt_ui.windows.GameUpdateSignal import GameUpdateSignal
from ..models import AtoModel, GameModel, NullListModel, PackageModel from ..models import AtoModel, GameModel, NullListModel, PackageModel
@ -33,6 +34,10 @@ class FlightDelegate(QStyledItemDelegate):
HMARGIN = 4 HMARGIN = 4
VMARGIN = 4 VMARGIN = 4
def __init__(self, package: Package) -> None:
super().__init__()
self.package = package
def get_font(self, option: QStyleOptionViewItem) -> QFont: def get_font(self, option: QStyleOptionViewItem) -> QFont:
font = QFont(option.font) font = QFont(option.font)
font.setPointSize(self.FONT_SIZE) font.setPointSize(self.FONT_SIZE)
@ -47,8 +52,9 @@ class FlightDelegate(QStyledItemDelegate):
task = flight.flight_type.name task = flight.flight_type.name
count = flight.count count = flight.count
name = db.unit_type_name(flight.unit_type) name = db.unit_type_name(flight.unit_type)
delay = flight.scheduled_in estimator = TotEstimator(self.package)
return f"[{task}] {count} x {name} in {delay} minutes" delay = datetime.timedelta(seconds=estimator.mission_start_time(flight))
return f"[{task}] {count} x {name} in {delay}"
def second_row_text(self, index: QModelIndex) -> str: def second_row_text(self, index: QModelIndex) -> str:
flight = self.flight(index) flight = self.flight(index)
@ -128,7 +134,8 @@ class QFlightList(QListView):
super().__init__() super().__init__()
self.package_model = model self.package_model = model
self.set_package(model) self.set_package(model)
self.setItemDelegate(FlightDelegate()) if model is not None:
self.setItemDelegate(FlightDelegate(model.package))
self.setIconSize(QSize(91, 24)) self.setIconSize(QSize(91, 24))
self.setSelectionBehavior(QAbstractItemView.SelectItems) self.setSelectionBehavior(QAbstractItemView.SelectItems)
@ -138,6 +145,7 @@ class QFlightList(QListView):
self.disconnect_model() self.disconnect_model()
else: else:
self.package_model = model self.package_model = model
self.setItemDelegate(FlightDelegate(model.package))
self.setModel(model) self.setModel(model)
# noinspection PyUnresolvedReferences # noinspection PyUnresolvedReferences
model.deleted.connect(self.disconnect_model) model.deleted.connect(self.disconnect_model)

View File

@ -21,7 +21,8 @@ from game import Game, db
from game.data.aaa_db import AAA_UNITS from game.data.aaa_db import AAA_UNITS
from game.data.radar_db import UNITS_WITH_RADAR from game.data.radar_db import UNITS_WITH_RADAR
from game.utils import meter_to_feet from game.utils import meter_to_feet
from gen import Conflict, Package, PackageWaypointTiming from gen import Conflict, PackageWaypointTiming
from gen.ato import Package
from gen.flights.flight import Flight, FlightWaypoint, FlightWaypointType from gen.flights.flight import Flight, FlightWaypoint, FlightWaypointType
from qt_ui.displayoptions import DisplayOptions from qt_ui.displayoptions import DisplayOptions
from qt_ui.models import GameModel from qt_ui.models import GameModel

View File

@ -1,26 +1,28 @@
import datetime
from PySide2.QtGui import QStandardItem, QIcon from PySide2.QtGui import QStandardItem, QIcon
from game import db from game import db
from gen.ato import Package
from gen.flights.flight import Flight from gen.flights.flight import Flight
from gen.flights.traveltime import TotEstimator
from qt_ui.uiconstants import AIRCRAFT_ICONS from qt_ui.uiconstants import AIRCRAFT_ICONS
# TODO: Replace with QFlightList.
class QFlightItem(QStandardItem): class QFlightItem(QStandardItem):
def __init__(self, flight:Flight): def __init__(self, package: Package, flight: Flight):
super(QFlightItem, self).__init__() super(QFlightItem, self).__init__()
self.package = package
self.flight = flight self.flight = flight
if db.unit_type_name(self.flight.unit_type).replace("/", " ") in AIRCRAFT_ICONS.keys(): if db.unit_type_name(self.flight.unit_type).replace("/", " ") in AIRCRAFT_ICONS.keys():
icon = QIcon((AIRCRAFT_ICONS[db.unit_type_name(self.flight.unit_type)])) icon = QIcon((AIRCRAFT_ICONS[db.unit_type_name(self.flight.unit_type)]))
self.setIcon(icon) self.setIcon(icon)
self.setEditable(False) self.setEditable(False)
estimator = TotEstimator(self.package)
delay = datetime.timedelta(seconds=estimator.mission_start_time(flight))
self.setText("["+str(self.flight.flight_type.name[:6])+"] " self.setText("["+str(self.flight.flight_type.name[:6])+"] "
+ str(self.flight.count) + " x " + db.unit_type_name(self.flight.unit_type) + str(self.flight.count) + " x " + db.unit_type_name(self.flight.unit_type)
+ " in " + str(self.flight.scheduled_in) + " minutes") + " in " + str(delay))
def update(self, flight):
self.flight = flight
self.setText("[" + str(self.flight.flight_type.name[:6]) + "] "
+ str(self.flight.count) + " x " + db.unit_type_name(self.flight.unit_type)
+ " in " + str(self.flight.scheduled_in) + " minutes")

View File

@ -116,11 +116,14 @@ class QPackageDialog(QDialog):
self.finished.connect(self.on_close) self.finished.connect(self.on_close)
def on_close(self, _result) -> None: @staticmethod
def on_close(_result) -> None:
GameUpdateSignal.get_instance().redraw_flight_paths()
def save_tot(self) -> None:
time = self.tot_spinner.time() time = self.tot_spinner.time()
seconds = time.hour() * 3600 + time.minute() * 60 + time.second() seconds = time.hour() * 3600 + time.minute() * 60 + time.second()
self.package_model.update_tot(seconds) self.package_model.update_tot(seconds)
GameUpdateSignal.get_instance().redraw_flight_paths()
def on_selection_changed(self, selected: QItemSelection, def on_selection_changed(self, selected: QItemSelection,
_deselected: QItemSelection) -> None: _deselected: QItemSelection) -> None:
@ -182,6 +185,7 @@ class QNewPackageDialog(QPackageDialog):
Empty packages may be created. They can be modified later, and will have Empty packages may be created. They can be modified later, and will have
no effect if empty when the mission is generated. no effect if empty when the mission is generated.
""" """
self.save_tot()
self.ato_model.add_package(self.package_model.package) self.ato_model.add_package(self.package_model.package)
for flight in self.package_model.package.flights: for flight in self.package_model.package.flights:
self.game.aircraft_inventory.claim_for_flight(flight) self.game.aircraft_inventory.claim_for_flight(flight)
@ -227,6 +231,7 @@ class QEditPackageDialog(QPackageDialog):
def on_done(self) -> None: def on_done(self) -> None:
"""Closes the window.""" """Closes the window."""
self.save_tot()
self.close() self.close()
def on_delete(self) -> None: def on_delete(self) -> None:

View File

@ -90,7 +90,11 @@ class QFlightCreator(QDialog):
origin = self.airfield_selector.currentData() origin = self.airfield_selector.currentData()
size = self.flight_size_spinner.value() size = self.flight_size_spinner.value()
flight = Flight(aircraft, size, origin, task) if self.game.settings.perf_ai_parking_start:
start_type = "Cold"
else:
start_type = "Warm"
flight = Flight(aircraft, size, origin, task, start_type)
flight.scheduled_in = self.package.delay flight.scheduled_in = self.package.delay
# noinspection PyUnresolvedReferences # noinspection PyUnresolvedReferences

View File

@ -1,6 +1,7 @@
from PySide2.QtWidgets import QLabel, QHBoxLayout, QGroupBox, QSpinBox from PySide2.QtWidgets import QLabel, QHBoxLayout, QGroupBox, QSpinBox
# TODO: Remove?
class QFlightDepartureEditor(QGroupBox): class QFlightDepartureEditor(QGroupBox):
def __init__(self, flight): def __init__(self, flight):
@ -15,7 +16,7 @@ class QFlightDepartureEditor(QGroupBox):
self.departure_delta = QSpinBox(self) self.departure_delta = QSpinBox(self)
self.departure_delta.setMinimum(0) self.departure_delta.setMinimum(0)
self.departure_delta.setMaximum(120) self.departure_delta.setMaximum(120)
self.departure_delta.setValue(self.flight.scheduled_in) self.departure_delta.setValue(self.flight.scheduled_in // 60)
self.departure_delta.valueChanged.connect(self.change_scheduled) self.departure_delta.valueChanged.connect(self.change_scheduled)
layout.addWidget(self.depart_from) layout.addWidget(self.depart_from)
@ -27,4 +28,4 @@ class QFlightDepartureEditor(QGroupBox):
self.changed = self.departure_delta.valueChanged self.changed = self.departure_delta.valueChanged
def change_scheduled(self): def change_scheduled(self):
self.flight.scheduled_in = int(self.departure_delta.value()) self.flight.scheduled_in = int(self.departure_delta.value() * 60)