Convert TOTs to datetime.

https://github.com/dcs-liberation/dcs_liberation/issues/1680
This commit is contained in:
Dan Albert 2022-09-02 20:58:26 -07:00 committed by Raffson
parent 625c4f2dfb
commit 59673e7911
No known key found for this signature in database
GPG Key ID: B0402B2C9B764D99
50 changed files with 333 additions and 239 deletions

View File

@ -1,7 +1,7 @@
from __future__ import annotations from __future__ import annotations
from dataclasses import dataclass from dataclasses import dataclass
from datetime import timedelta from datetime import datetime
from typing import Iterator, TYPE_CHECKING, Type from typing import Iterator, TYPE_CHECKING, Type
from game.theater.controlpoint import ControlPointType from game.theater.controlpoint import ControlPointType
@ -65,21 +65,21 @@ class AirAssaultFlightPlan(FormationAttackFlightPlan, UiZoneDisplay):
return self.layout.targets[0] return self.layout.targets[0]
@property @property
def ingress_time(self) -> timedelta: def ingress_time(self) -> datetime:
tot = self.tot tot = self.tot
travel_time = self.travel_time_between_waypoints( travel_time = self.travel_time_between_waypoints(
self.layout.ingress, self.tot_waypoint self.layout.ingress, self.tot_waypoint
) )
return tot - travel_time return tot - travel_time
def tot_for_waypoint(self, waypoint: FlightWaypoint) -> timedelta | None: def tot_for_waypoint(self, waypoint: FlightWaypoint) -> datetime | None:
if waypoint is self.tot_waypoint: if waypoint is self.tot_waypoint:
return self.tot return self.tot
elif waypoint is self.layout.ingress: elif waypoint is self.layout.ingress:
return self.ingress_time return self.ingress_time
return None return None
def depart_time_for_waypoint(self, waypoint: FlightWaypoint) -> timedelta | None: def depart_time_for_waypoint(self, waypoint: FlightWaypoint) -> datetime | None:
return None return None
@property @property
@ -87,7 +87,11 @@ class AirAssaultFlightPlan(FormationAttackFlightPlan, UiZoneDisplay):
return meters(2500) return meters(2500)
@property @property
def mission_departure_time(self) -> timedelta: def mission_begin_on_station_time(self) -> datetime | None:
return None
@property
def mission_departure_time(self) -> datetime:
return self.package.time_over_target return self.package.time_over_target
def ui_zone(self) -> UiZone: def ui_zone(self) -> UiZone:

View File

@ -2,8 +2,9 @@ from __future__ import annotations
from collections.abc import Iterator from collections.abc import Iterator
from dataclasses import dataclass from dataclasses import dataclass
from datetime import timedelta from datetime import datetime
from typing import TYPE_CHECKING, Type, Optional from typing import Optional
from typing import TYPE_CHECKING, Type
from game.theater.missiontarget import MissionTarget from game.theater.missiontarget import MissionTarget
from game.utils import feet from game.utils import feet
@ -89,16 +90,20 @@ class AirliftFlightPlan(StandardFlightPlan[AirliftLayout]):
# drop-off waypoint. # drop-off waypoint.
return self.layout.drop_off or self.layout.arrival return self.layout.drop_off or self.layout.arrival
def tot_for_waypoint(self, waypoint: FlightWaypoint) -> timedelta | None: def tot_for_waypoint(self, waypoint: FlightWaypoint) -> datetime | None:
# TOT planning isn't really useful for transports. They're behind the front # TOT planning isn't really useful for transports. They're behind the front
# lines so no need to wait for escorts or for other missions to complete. # lines so no need to wait for escorts or for other missions to complete.
return None return None
def depart_time_for_waypoint(self, waypoint: FlightWaypoint) -> timedelta | None: def depart_time_for_waypoint(self, waypoint: FlightWaypoint) -> datetime | None:
return None return None
@property @property
def mission_departure_time(self) -> timedelta: def mission_begin_on_station_time(self) -> datetime | None:
return None
@property
def mission_departure_time(self) -> datetime:
return self.package.time_over_target return self.package.time_over_target

View File

@ -2,7 +2,7 @@ from __future__ import annotations
from collections.abc import Iterator from collections.abc import Iterator
from dataclasses import dataclass from dataclasses import dataclass
from datetime import timedelta from datetime import datetime
from typing import TYPE_CHECKING, Type from typing import TYPE_CHECKING, Type
from .flightplan import FlightPlan, Layout from .flightplan import FlightPlan, Layout
@ -46,16 +46,20 @@ class CustomFlightPlan(FlightPlan[CustomLayout]):
return waypoint return waypoint
return self.layout.departure return self.layout.departure
def tot_for_waypoint(self, waypoint: FlightWaypoint) -> timedelta | None: def tot_for_waypoint(self, waypoint: FlightWaypoint) -> datetime | None:
if waypoint == self.tot_waypoint: if waypoint == self.tot_waypoint:
return self.package.time_over_target + self.tot_offset return self.package.time_over_target + self.tot_offset
return None return None
def depart_time_for_waypoint(self, waypoint: FlightWaypoint) -> timedelta | None: def depart_time_for_waypoint(self, waypoint: FlightWaypoint) -> datetime | None:
return None return None
@property @property
def mission_departure_time(self) -> timedelta: def mission_begin_on_station_time(self) -> datetime | None:
return None
@property
def mission_departure_time(self) -> datetime:
return self.package.time_over_target return self.package.time_over_target

View File

@ -2,7 +2,7 @@ from __future__ import annotations
from collections.abc import Iterator from collections.abc import Iterator
from dataclasses import dataclass from dataclasses import dataclass
from datetime import timedelta from datetime import datetime
from typing import TYPE_CHECKING, Type from typing import TYPE_CHECKING, Type
from game.utils import feet from game.utils import feet
@ -35,16 +35,20 @@ class FerryFlightPlan(StandardFlightPlan[FerryLayout]):
def tot_waypoint(self) -> FlightWaypoint: def tot_waypoint(self) -> FlightWaypoint:
return self.layout.arrival return self.layout.arrival
def tot_for_waypoint(self, waypoint: FlightWaypoint) -> timedelta | None: def tot_for_waypoint(self, waypoint: FlightWaypoint) -> datetime | None:
# TOT planning isn't really useful for ferries. They're behind the front # TOT planning isn't really useful for ferries. They're behind the front
# lines so no need to wait for escorts or for other missions to complete. # lines so no need to wait for escorts or for other missions to complete.
return None return None
def depart_time_for_waypoint(self, waypoint: FlightWaypoint) -> timedelta | None: def depart_time_for_waypoint(self, waypoint: FlightWaypoint) -> datetime | None:
return None return None
@property @property
def mission_departure_time(self) -> timedelta: def mission_begin_on_station_time(self) -> datetime | None:
return None
@property
def mission_departure_time(self) -> datetime:
return self.package.time_over_target return self.package.time_over_target

View File

@ -8,10 +8,10 @@ generating the waypoints for the mission.
from __future__ import annotations from __future__ import annotations
import math import math
from abc import ABC from abc import ABC, abstractmethod
from collections.abc import Iterator from collections.abc import Iterator
from dataclasses import dataclass from dataclasses import dataclass
from datetime import timedelta from datetime import datetime, timedelta
from functools import cached_property from functools import cached_property
from typing import Any, Generic, TYPE_CHECKING, TypeGuard, TypeVar from typing import Any, Generic, TYPE_CHECKING, TypeGuard, TypeVar
@ -159,7 +159,7 @@ class FlightPlan(ABC, Generic[LayoutT]):
raise NotImplementedError raise NotImplementedError
@property @property
def tot(self) -> timedelta: def tot(self) -> datetime:
return self.package.time_over_target + self.tot_offset return self.package.time_over_target + self.tot_offset
@cached_property @cached_property
@ -224,6 +224,8 @@ class FlightPlan(ABC, Generic[LayoutT]):
for previous_waypoint, waypoint in self.edges(until=destination): for previous_waypoint, waypoint in self.edges(until=destination):
total += self.total_time_between_waypoints(previous_waypoint, waypoint) total += self.total_time_between_waypoints(previous_waypoint, waypoint)
total += self.travel_time_between_waypoints(previous_waypoint, waypoint)
# Trim microseconds. Our simulation tick rate is 1 second, so anything that # Trim microseconds. Our simulation tick rate is 1 second, so anything that
# takes 100.1 or 100.9 seconds will take 100 seconds. DCS doesn't handle # takes 100.1 or 100.9 seconds will take 100 seconds. DCS doesn't handle
# sub-second resolution for tasks anyway, nor are they interesting from a # sub-second resolution for tasks anyway, nor are they interesting from a
@ -249,10 +251,10 @@ class FlightPlan(ABC, Generic[LayoutT]):
distance = meters(a.position.distance_to_point(b.position)) distance = meters(a.position.distance_to_point(b.position))
return timedelta(hours=distance.nautical_miles / speed.knots * error_factor) return timedelta(hours=distance.nautical_miles / speed.knots * error_factor)
def tot_for_waypoint(self, waypoint: FlightWaypoint) -> timedelta | None: def tot_for_waypoint(self, waypoint: FlightWaypoint) -> datetime | None:
raise NotImplementedError raise NotImplementedError
def depart_time_for_waypoint(self, waypoint: FlightWaypoint) -> timedelta | None: def depart_time_for_waypoint(self, waypoint: FlightWaypoint) -> datetime | None:
raise NotImplementedError raise NotImplementedError
def request_escort_at(self) -> FlightWaypoint | None: def request_escort_at(self) -> FlightWaypoint | None:
@ -275,12 +277,14 @@ class FlightPlan(ABC, Generic[LayoutT]):
if waypoint == end: if waypoint == end:
return return
def takeoff_time(self) -> timedelta: def takeoff_time(self) -> datetime:
return self.tot - self._travel_time_to_waypoint(self.tot_waypoint) return self.tot - self._travel_time_to_waypoint(self.tot_waypoint)
def startup_time(self) -> timedelta: def minimum_duration_from_start_to_tot(self) -> timedelta:
start_time = ( return (
self.takeoff_time() - self.estimate_startup() - self.estimate_ground_ops() self._travel_time_to_waypoint(self.tot_waypoint)
+ self.estimate_startup()
+ self.estimate_ground_ops()
) )
# In case FP math has given us some barely below zero time, round to # In case FP math has given us some barely below zero time, round to
@ -304,6 +308,11 @@ class FlightPlan(ABC, Generic[LayoutT]):
return start_time return start_time
def startup_time(self) -> datetime:
return (
self.takeoff_time() - self.estimate_startup() - self.estimate_ground_ops()
)
def estimate_startup(self) -> timedelta: def estimate_startup(self) -> timedelta:
if self.flight.start_type is StartType.COLD: if self.flight.start_type is StartType.COLD:
if self.flight.client_count: if self.flight.client_count:
@ -325,12 +334,16 @@ class FlightPlan(ABC, Generic[LayoutT]):
def is_airassault(self) -> bool: def is_airassault(self) -> bool:
return False return False
@abstractmethod
def mission_begin_on_station_time(self) -> datetime | None:
"""The time that the mission is first on-station."""
@property @property
def is_custom(self) -> bool: def is_custom(self) -> bool:
return False return False
@property @property
def mission_departure_time(self) -> timedelta: def mission_departure_time(self) -> datetime:
"""The time that the mission is complete and the flight RTBs.""" """The time that the mission is complete and the flight RTBs."""
raise NotImplementedError raise NotImplementedError

View File

@ -2,7 +2,7 @@ from __future__ import annotations
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from dataclasses import dataclass from dataclasses import dataclass
from datetime import timedelta from datetime import datetime, timedelta
from functools import cached_property from functools import cached_property
from typing import Any, TYPE_CHECKING, TypeGuard, Optional from typing import Any, TYPE_CHECKING, TypeGuard, Optional
@ -75,15 +75,15 @@ class FormationFlightPlan(LoiterFlightPlan, ABC):
@property @property
@abstractmethod @abstractmethod
def join_time(self) -> timedelta: def join_time(self) -> datetime:
... ...
@property @property
@abstractmethod @abstractmethod
def split_time(self) -> timedelta: def split_time(self) -> datetime:
... ...
def tot_for_waypoint(self, waypoint: FlightWaypoint) -> timedelta | None: def tot_for_waypoint(self, waypoint: FlightWaypoint) -> datetime | None:
if waypoint == self.layout.join: if waypoint == self.layout.join:
return self.join_time + self.tot_offset return self.join_time + self.tot_offset
elif waypoint == self.layout.split: elif waypoint == self.layout.split:
@ -91,13 +91,18 @@ class FormationFlightPlan(LoiterFlightPlan, ABC):
return None return None
@property @property
def push_time(self) -> timedelta: def push_time(self) -> datetime:
return self.join_time - self.travel_time_between_waypoints( return self.join_time - self.travel_time_between_waypoints(
self.layout.hold, self.layout.join self.layout.hold.position,
self.layout.join.position,
) )
@property @property
def mission_departure_time(self) -> timedelta: def mission_begin_on_station_time(self) -> datetime | None:
return None
@property
def mission_departure_time(self) -> datetime:
return self.split_time return self.split_time
@self_type_guard @self_type_guard

View File

@ -3,8 +3,9 @@ from __future__ import annotations
from abc import ABC from abc import ABC
from collections.abc import Iterator from collections.abc import Iterator
from dataclasses import dataclass from dataclasses import dataclass
from datetime import timedelta from datetime import datetime, timedelta
from typing import TYPE_CHECKING, TypeVar, Optional from typing import Optional
from typing import TYPE_CHECKING, TypeVar
from dcs import Point from dcs import Point
@ -14,6 +15,7 @@ from game.utils import Speed, meters, nautical_miles, feet
from .flightplan import FlightPlan from .flightplan import FlightPlan
from .formation import FormationFlightPlan, FormationLayout from .formation import FormationFlightPlan, FormationLayout
from .ibuilder import IBuilder from .ibuilder import IBuilder
from .planningerror import PlanningError
from .waypointbuilder import StrikeTarget, WaypointBuilder from .waypointbuilder import StrikeTarget, WaypointBuilder
from .. import FlightType from .. import FlightType
from ..flightwaypoint import FlightWaypoint from ..flightwaypoint import FlightWaypoint
@ -56,14 +58,37 @@ class FormationAttackFlightPlan(FormationFlightPlan, ABC):
) )
@property @property
def join_time(self) -> timedelta: def travel_time_to_target(self) -> timedelta:
"""The estimated time between the first waypoint and the target."""
destination = self.tot_waypoint
total = timedelta()
for previous_waypoint, waypoint in self.edges():
if waypoint == self.tot_waypoint:
# For anything strike-like the TOT waypoint is the *flight's*
# mission target, but to synchronize with the rest of the
# package we need to use the travel time to the same position as
# the others.
total += self.travel_time_between_waypoints(
previous_waypoint, self.target_area_waypoint
)
break
total += self.travel_time_between_waypoints(previous_waypoint, waypoint)
else:
raise PlanningError(
f"Did not find destination waypoint {destination} in "
f"waypoints for {self.flight}"
)
return total
@property
def join_time(self) -> datetime:
travel_time = self.total_time_between_waypoints( travel_time = self.total_time_between_waypoints(
self.layout.join, self.layout.ingress self.layout.join, self.layout.ingress
) )
return self.ingress_time - travel_time return self.ingress_time - travel_time
@property @property
def split_time(self) -> timedelta: def split_time(self) -> datetime:
travel_time_ingress = self.total_time_between_waypoints( travel_time_ingress = self.total_time_between_waypoints(
self.layout.ingress, self.target_area_waypoint self.layout.ingress, self.target_area_waypoint
) )
@ -80,7 +105,7 @@ class FormationAttackFlightPlan(FormationFlightPlan, ABC):
) )
@property @property
def ingress_time(self) -> timedelta: def ingress_time(self) -> datetime:
tot = self.tot tot = self.tot
travel_time = self.total_time_between_waypoints( travel_time = self.total_time_between_waypoints(
self.layout.ingress, self.target_area_waypoint self.layout.ingress, self.target_area_waypoint
@ -88,14 +113,14 @@ class FormationAttackFlightPlan(FormationFlightPlan, ABC):
return tot - travel_time return tot - travel_time
@property @property
def initial_time(self) -> timedelta: def initial_time(self) -> datetime:
tot = self.tot tot = self.tot
travel_time = self.travel_time_between_waypoints( travel_time = self.travel_time_between_waypoints(
self.layout.initial, self.target_area_waypoint self.layout.initial, self.target_area_waypoint
) )
return tot - travel_time return tot - travel_time
def tot_for_waypoint(self, waypoint: FlightWaypoint) -> timedelta | None: def tot_for_waypoint(self, waypoint: FlightWaypoint) -> datetime | None:
if waypoint == self.layout.ingress: if waypoint == self.layout.ingress:
return self.ingress_time return self.ingress_time
elif waypoint == self.layout.initial: elif waypoint == self.layout.initial:

View File

@ -2,8 +2,9 @@ from __future__ import annotations
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from dataclasses import dataclass from dataclasses import dataclass
from datetime import timedelta from datetime import datetime, timedelta
from typing import Any, TYPE_CHECKING, TypeGuard, Optional from typing import Any, TYPE_CHECKING, TypeGuard
from typing import Optional
from game.typeguard import self_type_guard from game.typeguard import self_type_guard
from .flightplan import FlightPlan from .flightplan import FlightPlan
@ -25,10 +26,10 @@ class LoiterFlightPlan(StandardFlightPlan[Any], ABC):
@property @property
@abstractmethod @abstractmethod
def push_time(self) -> timedelta: def push_time(self) -> datetime:
... ...
def depart_time_for_waypoint(self, waypoint: FlightWaypoint) -> timedelta | None: def depart_time_for_waypoint(self, waypoint: FlightWaypoint) -> datetime | None:
if waypoint == self.layout.hold: if waypoint == self.layout.hold:
return self.push_time + self.tot_offset return self.push_time + self.tot_offset
return None return None

View File

@ -1,6 +1,6 @@
from __future__ import annotations from __future__ import annotations
from datetime import timedelta from datetime import datetime, timedelta
from typing import Type from typing import Type
from dcs import Point from dcs import Point
@ -39,7 +39,7 @@ class PackageRefuelingFlightPlan(RefuelingFlightPlan):
) )
@property @property
def patrol_start_time(self) -> timedelta: def patrol_start_time(self) -> datetime:
altitude = self.flight.unit_type.patrol_altitude altitude = self.flight.unit_type.patrol_altitude
if altitude is None: if altitude is None:

View File

@ -3,7 +3,7 @@ from __future__ import annotations
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from collections.abc import Iterator from collections.abc import Iterator
from dataclasses import dataclass from dataclasses import dataclass
from datetime import timedelta from datetime import datetime, timedelta
from typing import Any, TYPE_CHECKING, TypeGuard, TypeVar from typing import Any, TYPE_CHECKING, TypeGuard, TypeVar
from game.ato.flightplans.standard import StandardFlightPlan, StandardLayout from game.ato.flightplans.standard import StandardFlightPlan, StandardLayout
@ -59,22 +59,22 @@ class PatrollingFlightPlan(StandardFlightPlan[LayoutT], UiZoneDisplay, ABC):
""" """
@property @property
def patrol_start_time(self) -> timedelta: def patrol_start_time(self) -> datetime:
return self.tot return self.tot
@property @property
def patrol_end_time(self) -> timedelta: def patrol_end_time(self) -> datetime:
# TODO: This is currently wrong for CAS. # TODO: This is currently wrong for CAS.
# CAS missions end when they're winchester or bingo. We need to # CAS missions end when they're winchester or bingo. We need to
# configure push tasks for the escorts rather than relying on timing. # configure push tasks for the escorts rather than relying on timing.
return self.patrol_start_time + self.patrol_duration return self.patrol_start_time + self.patrol_duration
def tot_for_waypoint(self, waypoint: FlightWaypoint) -> timedelta | None: def tot_for_waypoint(self, waypoint: FlightWaypoint) -> datetime | None:
if waypoint == self.layout.patrol_start: if waypoint == self.layout.patrol_start:
return self.patrol_start_time return self.patrol_start_time
return None return None
def depart_time_for_waypoint(self, waypoint: FlightWaypoint) -> timedelta | None: def depart_time_for_waypoint(self, waypoint: FlightWaypoint) -> datetime | None:
if waypoint == self.layout.patrol_end: if waypoint == self.layout.patrol_end:
return self.patrol_end_time return self.patrol_end_time
return None return None
@ -88,7 +88,11 @@ class PatrollingFlightPlan(StandardFlightPlan[LayoutT], UiZoneDisplay, ABC):
return self.layout.patrol_start return self.layout.patrol_start
@property @property
def mission_departure_time(self) -> timedelta: def mission_begin_on_station_time(self) -> datetime:
return self.patrol_start_time
@property
def mission_departure_time(self) -> datetime:
return self.patrol_end_time return self.patrol_end_time
@self_type_guard @self_type_guard

View File

@ -2,7 +2,7 @@ from __future__ import annotations
from collections.abc import Iterator from collections.abc import Iterator
from dataclasses import dataclass from dataclasses import dataclass
from datetime import timedelta from datetime import datetime
from typing import TYPE_CHECKING, Type from typing import TYPE_CHECKING, Type
from game.utils import feet from game.utils import feet
@ -42,15 +42,19 @@ class RtbFlightPlan(StandardFlightPlan[RtbLayout]):
def tot_waypoint(self) -> FlightWaypoint: def tot_waypoint(self) -> FlightWaypoint:
return self.layout.abort_location return self.layout.abort_location
def tot_for_waypoint(self, waypoint: FlightWaypoint) -> timedelta | None: def tot_for_waypoint(self, waypoint: FlightWaypoint) -> datetime | None:
return None return None
def depart_time_for_waypoint(self, waypoint: FlightWaypoint) -> timedelta | None: def depart_time_for_waypoint(self, waypoint: FlightWaypoint) -> datetime | None:
return None return None
@property @property
def mission_departure_time(self) -> timedelta: def mission_begin_on_station_time(self) -> datetime | None:
return timedelta() return None
@property
def mission_departure_time(self) -> datetime:
return self.tot
class Builder(IBuilder[RtbFlightPlan, RtbLayout]): class Builder(IBuilder[RtbFlightPlan, RtbLayout]):

View File

@ -1,7 +1,7 @@
from __future__ import annotations from __future__ import annotations
from dataclasses import dataclass from dataclasses import dataclass
from datetime import timedelta from datetime import datetime, timedelta
from typing import Iterator, TYPE_CHECKING, Type from typing import Iterator, TYPE_CHECKING, Type
from dcs import Point from dcs import Point
@ -54,36 +54,40 @@ class SweepFlightPlan(LoiterFlightPlan):
return -timedelta(minutes=5) return -timedelta(minutes=5)
@property @property
def sweep_start_time(self) -> timedelta: def sweep_start_time(self) -> datetime:
travel_time = self.total_time_between_waypoints( travel_time = self.total_time_between_waypoints(
self.layout.sweep_start, self.layout.sweep_end self.layout.sweep_start, self.layout.sweep_end
) )
return self.sweep_end_time - travel_time return self.sweep_end_time - travel_time
@property @property
def sweep_end_time(self) -> timedelta: def sweep_end_time(self) -> datetime:
return self.tot return self.tot
def tot_for_waypoint(self, waypoint: FlightWaypoint) -> timedelta | None: def tot_for_waypoint(self, waypoint: FlightWaypoint) -> datetime | None:
if waypoint == self.layout.sweep_start: if waypoint == self.layout.sweep_start:
return self.sweep_start_time + self.tot_offset return self.sweep_start_time + self.tot_offset
if waypoint == self.layout.sweep_end: if waypoint == self.layout.sweep_end:
return self.sweep_end_time + self.tot_offset return self.sweep_end_time + self.tot_offset
return None return None
def depart_time_for_waypoint(self, waypoint: FlightWaypoint) -> timedelta | None: def depart_time_for_waypoint(self, waypoint: FlightWaypoint) -> datetime | None:
if waypoint == self.layout.hold: if waypoint == self.layout.hold:
return self.push_time + self.tot_offset return self.push_time + self.tot_offset
return None return None
@property @property
def push_time(self) -> timedelta: def push_time(self) -> datetime:
return self.sweep_end_time - self.travel_time_between_waypoints( return self.sweep_end_time - self.travel_time_between_waypoints(
self.layout.hold, self.layout.sweep_end self.layout.hold.position,
self.layout.sweep_end.position,
) )
@property @property
def mission_departure_time(self) -> timedelta: def mission_begin_on_station_time(self) -> datetime | None:
return None
def mission_departure_time(self) -> datetime:
return self.sweep_end_time return self.sweep_end_time

View File

@ -3,7 +3,7 @@ from __future__ import annotations
import random import random
from collections.abc import Iterator from collections.abc import Iterator
from dataclasses import dataclass from dataclasses import dataclass
from datetime import timedelta from datetime import datetime, timedelta
from typing import TYPE_CHECKING, Type from typing import TYPE_CHECKING, Type
from game.utils import Distance, Speed, feet from game.utils import Distance, Speed, feet
@ -71,20 +71,20 @@ class TarCapFlightPlan(PatrollingFlightPlan[TarCapLayout]):
def default_tot_offset(self) -> timedelta: def default_tot_offset(self) -> timedelta:
return -timedelta(minutes=2) return -timedelta(minutes=2)
def depart_time_for_waypoint(self, waypoint: FlightWaypoint) -> timedelta | None: def depart_time_for_waypoint(self, waypoint: FlightWaypoint) -> datetime | None:
if waypoint == self.layout.patrol_end: if waypoint == self.layout.patrol_end:
return self.patrol_end_time return self.patrol_end_time
return super().depart_time_for_waypoint(waypoint) return super().depart_time_for_waypoint(waypoint)
@property @property
def patrol_start_time(self) -> timedelta: def patrol_start_time(self) -> datetime:
start = self.package.escort_start_time start = self.package.escort_start_time
if start is not None: if start is not None:
return start + self.tot_offset return start + self.tot_offset
return self.tot return self.tot
@property @property
def patrol_end_time(self) -> timedelta: def patrol_end_time(self) -> datetime:
end = self.package.escort_end_time end = self.package.escort_end_time
if end is not None: if end is not None:
return end return end

View File

@ -35,7 +35,6 @@ class Uninitialized(FlightState):
@property @property
def description(self) -> str: def description(self) -> str:
delay = self.flight.flight_plan.startup_time()
if self.flight.start_type is StartType.COLD: if self.flight.start_type is StartType.COLD:
action = "Starting up" action = "Starting up"
elif self.flight.start_type is StartType.WARM: elif self.flight.start_type is StartType.WARM:
@ -46,4 +45,4 @@ class Uninitialized(FlightState):
action = "In flight" action = "In flight"
else: else:
raise ValueError(f"Unhandled StartType: {self.flight.start_type}") raise ValueError(f"Unhandled StartType: {self.flight.start_type}")
return f"{action} in {delay}" return f"{action} at {self.flight.flight_plan.startup_time():%H:%M:%S}"

View File

@ -1,7 +1,7 @@
from __future__ import annotations from __future__ import annotations
from dataclasses import dataclass, field from dataclasses import dataclass, field
from datetime import timedelta from datetime import datetime
from typing import Literal, TYPE_CHECKING from typing import Literal, TYPE_CHECKING
from dcs import Point from dcs import Point
@ -43,8 +43,8 @@ class FlightWaypoint:
# generation). We do it late so that we don't need to propagate changes # generation). We do it late so that we don't need to propagate changes
# to waypoint times whenever the player alters the package TOT or the # to waypoint times whenever the player alters the package TOT or the
# flight's offset in the UI. # flight's offset in the UI.
tot: timedelta | None = None tot: datetime | None = None
departure_time: timedelta | None = None departure_time: datetime | None = None
@property @property
def x(self) -> float: def x(self) -> float:

View File

@ -2,7 +2,7 @@ from __future__ import annotations
import logging import logging
from collections import defaultdict from collections import defaultdict
from datetime import timedelta from datetime import datetime
from typing import Dict, Optional, TYPE_CHECKING from typing import Dict, Optional, TYPE_CHECKING
from game.db import Database from game.db import Database
@ -42,8 +42,11 @@ class Package(RadioFrequencyContainer):
self.auto_asap = auto_asap self.auto_asap = auto_asap
self.flights: list[Flight] = [] self.flights: list[Flight] = []
# Desired TOT as an offset from mission start. # Desired TOT as an offset from mission start. Obviously datetime.min is bogus,
self.time_over_target: timedelta = timedelta() # but it's going to be replaced by whatever is scheduling the package very soon.
# TODO: Constructor should maybe take the current time and use that to preserve
# the old behavior?
self.time_over_target: datetime = datetime.min
self.waypoints: PackageWaypoints | None = None self.waypoints: PackageWaypoints | None = None
@property @property
@ -71,7 +74,7 @@ class Package(RadioFrequencyContainer):
# TODO: Should depend on the type of escort. # TODO: Should depend on the type of escort.
# SEAD might be able to leave before CAP. # SEAD might be able to leave before CAP.
@property @property
def escort_start_time(self) -> Optional[timedelta]: def escort_start_time(self) -> datetime | None:
times = [] times = []
for flight in self.flights: for flight in self.flights:
waypoint = flight.flight_plan.request_escort_at() waypoint = flight.flight_plan.request_escort_at()
@ -90,7 +93,7 @@ class Package(RadioFrequencyContainer):
return None return None
@property @property
def escort_end_time(self) -> Optional[timedelta]: def escort_end_time(self) -> datetime | None:
times = [] times = []
for flight in self.flights: for flight in self.flights:
waypoint = flight.flight_plan.dismiss_escort_at() waypoint = flight.flight_plan.dismiss_escort_at()
@ -112,7 +115,7 @@ class Package(RadioFrequencyContainer):
return None return None
@property @property
def mission_departure_time(self) -> Optional[timedelta]: def mission_departure_time(self) -> datetime | None:
times = [] times = []
for flight in self.flights: for flight in self.flights:
times.append(flight.flight_plan.mission_departure_time) times.append(flight.flight_plan.mission_departure_time)
@ -120,8 +123,8 @@ class Package(RadioFrequencyContainer):
return max(times) return max(times)
return None return None
def set_tot_asap(self) -> None: def set_tot_asap(self, now: datetime) -> None:
self.time_over_target = TotEstimator(self).earliest_tot() self.time_over_target = TotEstimator(self).earliest_tot(now)
def add_flight(self, flight: Flight) -> None: def add_flight(self, flight: Flight) -> None:
"""Adds a flight to the package.""" """Adds a flight to the package."""

View File

@ -1,7 +1,6 @@
from __future__ import annotations from __future__ import annotations
import math from datetime import datetime, timedelta
from datetime import timedelta
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from game.utils import Distance, SPEED_OF_SOUND_AT_SEA_LEVEL, Speed, mach, meters from game.utils import Distance, SPEED_OF_SOUND_AT_SEA_LEVEL, Speed, mach, meters
@ -40,44 +39,24 @@ class TotEstimator:
def __init__(self, package: Package) -> None: def __init__(self, package: Package) -> None:
self.package = package self.package = package
def earliest_tot(self) -> timedelta: def earliest_tot(self, now: datetime) -> datetime:
if not self.package.flights: if not self.package.flights:
return timedelta(0) return now
earliest_tot = max( return max(self.earliest_tot_for_flight(f, now) for f in self.package.flights)
(self.earliest_tot_for_flight(f) for f in self.package.flights)
)
# Trim microseconds. DCS doesn't handle sub-second resolution for tasks,
# and they're not interesting from a mission planning perspective so we
# don't want them in the UI.
#
# Round up so we don't get negative start times.
return timedelta(seconds=math.ceil(earliest_tot.total_seconds()))
@staticmethod @staticmethod
def earliest_tot_for_flight(flight: Flight) -> timedelta: def earliest_tot_for_flight(flight: Flight, now: datetime) -> datetime:
"""Estimate the fastest time from mission start to the target position. """Estimate the earliest time the flight can reach the target position.
For BARCAP flights, this is time to the racetrack start. This ensures that The interpretation of the TOT depends on the flight plan type. See the various
they are on station at the same time any other package members reach FlightPlan implementations for details.
their ingress point.
For other mission types this is the time to the mission target.
Args: Args:
flight: The flight to get the earliest TOT time for. flight: The flight to get the earliest TOT for.
now: The current mission time.
Returns: Returns:
The earliest possible TOT for the given flight in seconds. Returns 0 The earliest possible TOT for the given flight.
if an ingress point cannot be found.
""" """
# Clear the TOT, calculate the startup time. Negating the result gives return now + flight.flight_plan.minimum_duration_from_start_to_tot()
# the earliest possible start time.
orig_tot = flight.package.time_over_target
try:
flight.package.time_over_target = timedelta()
time = flight.flight_plan.startup_time()
finally:
flight.package.time_over_target = orig_tot
return -time

View File

@ -1,5 +1,6 @@
from __future__ import annotations from __future__ import annotations
from datetime import datetime
from typing import Any, Optional, TYPE_CHECKING from typing import Any, Optional, TYPE_CHECKING
from faker import Faker from faker import Faker
@ -182,10 +183,10 @@ class Coalition:
with logged_duration("Procurement of airlift assets"): with logged_duration("Procurement of airlift assets"):
self.transfers.order_airlift_assets() self.transfers.order_airlift_assets()
with logged_duration("Transport planning"): with logged_duration("Transport planning"):
self.transfers.plan_transports() self.transfers.plan_transports(self.game.conditions.start_time)
if not is_turn_0: if not is_turn_0:
self.plan_missions() self.plan_missions(self.game.conditions.start_time)
self.plan_procurement() self.plan_procurement()
def refund_outstanding_orders(self) -> None: def refund_outstanding_orders(self) -> None:
@ -201,16 +202,16 @@ class Coalition:
for squadron in self.air_wing.iter_squadrons(): for squadron in self.air_wing.iter_squadrons():
squadron.refund_orders() squadron.refund_orders()
def plan_missions(self) -> None: def plan_missions(self, now: datetime) -> None:
color = "Blue" if self.player else "Red" color = "Blue" if self.player else "Red"
with MultiEventTracer() as tracer: with MultiEventTracer() as tracer:
with tracer.trace(f"{color} mission planning"): with tracer.trace(f"{color} mission planning"):
with tracer.trace(f"{color} mission identification"): with tracer.trace(f"{color} mission identification"):
TheaterCommander(self.game, self.player).plan_missions(tracer) TheaterCommander(self.game, self.player).plan_missions(now, tracer)
with tracer.trace(f"{color} mission scheduling"): with tracer.trace(f"{color} mission scheduling"):
MissionScheduler( MissionScheduler(
self, self.game.settings.desired_player_mission_duration self, self.game.settings.desired_player_mission_duration
).schedule_missions() ).schedule_missions(now)
def plan_procurement(self) -> None: def plan_procurement(self) -> None:
# The first turn needs to buy a *lot* of aircraft to fill CAPs, so it gets much # The first turn needs to buy a *lot* of aircraft to fill CAPs, so it gets much

View File

@ -3,8 +3,8 @@ from __future__ import annotations
import logging import logging
import random import random
from collections import defaultdict from collections import defaultdict
from datetime import timedelta from datetime import datetime, timedelta
from typing import Iterator, Dict, TYPE_CHECKING from typing import Iterator, TYPE_CHECKING
from game.ato.flighttype import FlightType from game.ato.flighttype import FlightType
from game.ato.traveltime import TotEstimator from game.ato.traveltime import TotEstimator
@ -19,7 +19,7 @@ class MissionScheduler:
self.coalition = coalition self.coalition = coalition
self.desired_mission_length = desired_mission_length self.desired_mission_length = desired_mission_length
def schedule_missions(self) -> None: def schedule_missions(self, now: datetime) -> None:
"""Identifies and plans mission for the turn.""" """Identifies and plans mission for the turn."""
def start_time_generator( def start_time_generator(
@ -35,7 +35,7 @@ class MissionScheduler:
FlightType.TARCAP, FlightType.TARCAP,
} }
previous_cap_end_time: Dict[MissionTarget, timedelta] = defaultdict(timedelta) previous_cap_end_time: dict[MissionTarget, datetime] = defaultdict(now.replace)
non_dca_packages = [ non_dca_packages = [
p for p in self.coalition.ato.packages if p.primary_task not in dca_types p for p in self.coalition.ato.packages if p.primary_task not in dca_types
] ]
@ -47,7 +47,7 @@ class MissionScheduler:
margin=5 * 60, margin=5 * 60,
) )
for package in self.coalition.ato.packages: for package in self.coalition.ato.packages:
tot = TotEstimator(package).earliest_tot() tot = TotEstimator(package).earliest_tot(now)
if package.primary_task in dca_types: if package.primary_task in dca_types:
previous_end_time = previous_cap_end_time[package.target] previous_end_time = previous_cap_end_time[package.target]
if tot > previous_end_time: if tot > previous_end_time:
@ -65,7 +65,7 @@ class MissionScheduler:
continue continue
previous_cap_end_time[package.target] = departure_time previous_cap_end_time[package.target] = departure_time
elif package.auto_asap: elif package.auto_asap:
package.set_tot_asap() package.set_tot_asap(now)
else: else:
# But other packages should be spread out a bit. Note that take # But other packages should be spread out a bit. Note that take
# times are delayed, but all aircraft will become active at # times are delayed, but all aircraft will become active at

View File

@ -2,6 +2,7 @@ from __future__ import annotations
import logging import logging
from collections import defaultdict from collections import defaultdict
from datetime import datetime
from typing import Dict, Iterable, Optional, Set, TYPE_CHECKING from typing import Dict, Iterable, Optional, Set, TYPE_CHECKING
from game.ato.airtaaskingorder import AirTaskingOrder from game.ato.airtaaskingorder import AirTaskingOrder
@ -135,6 +136,7 @@ class PackageFulfiller:
self, self,
mission: ProposedMission, mission: ProposedMission,
purchase_multiplier: int, purchase_multiplier: int,
now: datetime,
tracer: MultiEventTracer, tracer: MultiEventTracer,
) -> Optional[Package]: ) -> Optional[Package]:
"""Allocates aircraft for a proposed mission and adds it to the ATO.""" """Allocates aircraft for a proposed mission and adds it to the ATO."""
@ -225,6 +227,6 @@ class PackageFulfiller:
if package.has_players and self.player_missions_asap: if package.has_players and self.player_missions_asap:
package.auto_asap = True package.auto_asap = True
package.set_tot_asap() package.set_tot_asap(now)
return package return package

View File

@ -105,6 +105,7 @@ class PackagePlanningTask(TheaterCommanderTask, Generic[MissionTargetT]):
self.package = fulfiller.plan_mission( self.package = fulfiller.plan_mission(
ProposedMission(self.target, self.flights), ProposedMission(self.target, self.flights),
self.purchase_multiplier, self.purchase_multiplier,
state.context.now,
state.context.tracer, state.context.tracer,
) )
return self.package is not None return self.package is not None

View File

@ -54,6 +54,7 @@ https://en.wikipedia.org/wiki/Hierarchical_task_network
""" """
from __future__ import annotations from __future__ import annotations
from datetime import datetime
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from game.ato.starttype import StartType from game.ato.starttype import StartType
@ -77,8 +78,8 @@ class TheaterCommander(Planner[TheaterState, TheaterCommanderTask]):
self.game = game self.game = game
self.player = player self.player = player
def plan_missions(self, tracer: MultiEventTracer) -> None: def plan_missions(self, now: datetime, tracer: MultiEventTracer) -> None:
state = TheaterState.from_game(self.game, self.player, tracer) state = TheaterState.from_game(self.game, self.player, now, tracer)
while True: while True:
result = self.plan(state) result = self.plan(state)
if result is None: if result is None:

View File

@ -5,6 +5,7 @@ import itertools
import math import math
from collections.abc import Iterator from collections.abc import Iterator
from dataclasses import dataclass from dataclasses import dataclass
from datetime import datetime
from typing import Optional, TYPE_CHECKING, Union from typing import Optional, TYPE_CHECKING, Union
from game.commander.battlepositions import BattlePositions from game.commander.battlepositions import BattlePositions
@ -36,6 +37,7 @@ class PersistentContext:
coalition: Coalition coalition: Coalition
theater: ConflictTheater theater: ConflictTheater
turn: int turn: int
now: datetime
settings: Settings settings: Settings
tracer: MultiEventTracer tracer: MultiEventTracer
@ -139,14 +141,20 @@ class TheaterState(WorldState["TheaterState"]):
@classmethod @classmethod
def from_game( def from_game(
cls, game: Game, player: bool, tracer: MultiEventTracer cls, game: Game, player: bool, now: datetime, tracer: MultiEventTracer
) -> TheaterState: ) -> TheaterState:
coalition = game.coalition_for(player) coalition = game.coalition_for(player)
finder = ObjectiveFinder(game, player) finder = ObjectiveFinder(game, player)
ordered_capturable_points = finder.prioritized_unisolated_points() ordered_capturable_points = finder.prioritized_unisolated_points()
context = PersistentContext( context = PersistentContext(
game.db, coalition, game.theater, game.turn, game.settings, tracer game.db,
coalition,
game.theater,
game.turn,
now,
game.settings,
tracer,
) )
# Plan enough rounds of CAP that the target has coverage over the expected # Plan enough rounds of CAP that the target has coverage over the expected

View File

@ -2,6 +2,7 @@ from __future__ import annotations
import logging import logging
from collections import defaultdict from collections import defaultdict
from datetime import datetime
from typing import Optional, TYPE_CHECKING from typing import Optional, TYPE_CHECKING
from game.theater import ControlPoint from game.theater import ControlPoint
@ -52,7 +53,7 @@ class GroundUnitOrders:
pending_units = 0 pending_units = 0
return pending_units return pending_units
def process(self, game: Game) -> None: def process(self, game: Game, now: datetime) -> None:
coalition = game.coalition_for(self.destination.captured) coalition = game.coalition_for(self.destination.captured)
ground_unit_source = self.find_ground_unit_source(game) ground_unit_source = self.find_ground_unit_source(game)
if ground_unit_source is None: if ground_unit_source is None:
@ -95,15 +96,20 @@ class GroundUnitOrders:
"still tried to transfer units to there" "still tried to transfer units to there"
) )
ground_unit_source.base.commission_units(units_needing_transfer) ground_unit_source.base.commission_units(units_needing_transfer)
self.create_transfer(coalition, ground_unit_source, units_needing_transfer) self.create_transfer(
coalition, ground_unit_source, units_needing_transfer, now
)
def create_transfer( def create_transfer(
self, self,
coalition: Coalition, coalition: Coalition,
source: ControlPoint, source: ControlPoint,
units: dict[GroundUnitType, int], units: dict[GroundUnitType, int],
now: datetime,
) -> None: ) -> None:
coalition.transfers.new_transfer(TransferOrder(source, self.destination, units)) coalition.transfers.new_transfer(
TransferOrder(source, self.destination, units), now
)
def find_ground_unit_source(self, game: Game) -> Optional[ControlPoint]: def find_ground_unit_source(self, game: Game) -> Optional[ControlPoint]:
# This is running *after* the turn counter has been incremented, so this is the # This is running *after* the turn counter has been incremented, so this is the

View File

@ -97,7 +97,6 @@ class FlightGroupConfigurator:
self.flight, self.flight,
self.group, self.group,
self.mission, self.mission,
self.game.conditions.start_time,
self.time, self.time,
self.game.settings, self.game.settings,
self.mission_data, self.mission_data,

View File

@ -31,7 +31,7 @@ class HoldPointBuilder(PydcsWaypointBuilder):
return return
push_time = self.flight.flight_plan.push_time push_time = self.flight.flight_plan.push_time
self.waypoint.departure_time = push_time self.waypoint.departure_time = push_time
elapsed = int((push_time - self.elapsed_mission_time).total_seconds()) - 60 elapsed = int((push_time - self.now).total_seconds()) - 60
loiter.stop_after_time(elapsed) loiter.stop_after_time(elapsed)
# What follows is some code to cope with the broken 'stop after time' condition # What follows is some code to cope with the broken 'stop after time' condition
create_stop_orbit_trigger(loiter, self.package, self.mission, elapsed) create_stop_orbit_trigger(loiter, self.package, self.mission, elapsed)

View File

@ -1,6 +1,6 @@
from __future__ import annotations from __future__ import annotations
from datetime import timedelta from datetime import datetime
from typing import Any, Iterable, Union from typing import Any, Iterable, Union
from dcs import Mission from dcs import Mission
@ -28,7 +28,7 @@ class PydcsWaypointBuilder:
group: FlyingGroup[Any], group: FlyingGroup[Any],
flight: Flight, flight: Flight,
mission: Mission, mission: Mission,
elapsed_mission_time: timedelta, now: datetime,
mission_data: MissionData, mission_data: MissionData,
) -> None: ) -> None:
self.waypoint = waypoint self.waypoint = waypoint
@ -36,7 +36,7 @@ class PydcsWaypointBuilder:
self.package = flight.package self.package = flight.package
self.flight = flight self.flight = flight
self.mission = mission self.mission = mission
self.elapsed_mission_time = elapsed_mission_time self.now = now
self.mission_data = mission_data self.mission_data = mission_data
def dcs_name_for_waypoint(self) -> str: def dcs_name_for_waypoint(self) -> str:
@ -83,10 +83,10 @@ class PydcsWaypointBuilder:
def add_tasks(self, waypoint: MovingPoint) -> None: def add_tasks(self, waypoint: MovingPoint) -> None:
pass pass
def set_waypoint_tot(self, waypoint: MovingPoint, tot: timedelta) -> None: def set_waypoint_tot(self, waypoint: MovingPoint, tot: datetime) -> None:
self.waypoint.tot = tot self.waypoint.tot = tot
if not self._viggen_client_tot(): if not self._viggen_client_tot():
waypoint.ETA = int((tot - self.elapsed_mission_time).total_seconds()) waypoint.ETA = int((tot - self.now).total_seconds())
waypoint.ETA_locked = True waypoint.ETA_locked = True
waypoint.speed_locked = False waypoint.speed_locked = False

View File

@ -57,7 +57,7 @@ class RaceTrackBuilder(PydcsWaypointBuilder):
racetrack = ControlledTask(orbit) racetrack = ControlledTask(orbit)
self.set_waypoint_tot(waypoint, flight_plan.patrol_start_time) self.set_waypoint_tot(waypoint, flight_plan.patrol_start_time)
loiter_duration = flight_plan.patrol_end_time - self.elapsed_mission_time loiter_duration = flight_plan.patrol_end_time - self.now
elapsed = int(loiter_duration.total_seconds()) elapsed = int(loiter_duration.total_seconds())
racetrack.stop_after_time(elapsed) racetrack.stop_after_time(elapsed)
# What follows is some code to cope with the broken 'stop after time' condition # What follows is some code to cope with the broken 'stop after time' condition

View File

@ -50,7 +50,6 @@ class WaypointGenerator:
flight: Flight, flight: Flight,
group: FlyingGroup[Any], group: FlyingGroup[Any],
mission: Mission, mission: Mission,
turn_start_time: datetime,
time: datetime, time: datetime,
settings: Settings, settings: Settings,
mission_data: MissionData, mission_data: MissionData,
@ -58,7 +57,6 @@ class WaypointGenerator:
self.flight = flight self.flight = flight
self.group = group self.group = group
self.mission = mission self.mission = mission
self.elapsed_mission_time = time - turn_start_time
self.time = time self.time = time
self.settings = settings self.settings = settings
self.mission_data = mission_data self.mission_data = mission_data
@ -152,7 +150,7 @@ class WaypointGenerator:
self.group, self.group,
self.flight, self.flight,
self.mission, self.mission,
self.elapsed_mission_time, self.time,
self.mission_data, self.mission_data,
) )
@ -183,12 +181,29 @@ class WaypointGenerator:
a.min_fuel = min_fuel a.min_fuel = min_fuel
def set_takeoff_time(self, waypoint: FlightWaypoint) -> timedelta: def set_takeoff_time(self, waypoint: FlightWaypoint) -> timedelta:
force_delay = False
if isinstance(self.flight.state, WaitingForStart): if isinstance(self.flight.state, WaitingForStart):
delay = self.flight.state.time_remaining(self.time) delay = self.flight.state.time_remaining(self.time)
elif (
# The first two clauses capture the flight states that we want to adjust. We
# don't want to delay any flights that are already in flight or on the
# runway.
not self.flight.state.in_flight
and self.flight.state.spawn_type is not StartType.RUNWAY
and self.flight.departure.is_fleet
and not self.flight.client_count
):
# https://github.com/dcs-liberation/dcs_liberation/issues/1309
# Without a delay, AI aircraft will be spawned on the sixpack, which other
# AI planes of course want to taxi through, deadlocking the carrier deck.
# Delaying AI carrier deck spawns by one second for some reason causes DCS
# to spawn those aircraft elsewhere, avoiding the traffic jam.
delay = timedelta(seconds=1)
force_delay = True
else: else:
delay = timedelta() delay = timedelta()
if self.should_delay_flight(): if force_delay or self.should_delay_flight():
if self.should_activate_late(): if self.should_activate_late():
# Late activation causes the aircraft to not be spawned # Late activation causes the aircraft to not be spawned
# until triggered. # until triggered.

View File

@ -5,7 +5,6 @@ from __future__ import annotations
import os import os
from dataclasses import dataclass from dataclasses import dataclass
from datetime import timedelta
from typing import Dict, List, TYPE_CHECKING from typing import Dict, List, TYPE_CHECKING
from dcs.mission import Mission from dcs.mission import Mission
@ -130,11 +129,9 @@ class MissionInfoGenerator:
def format_waypoint_time(waypoint: FlightWaypoint, depart_prefix: str) -> str: def format_waypoint_time(waypoint: FlightWaypoint, depart_prefix: str) -> str:
if waypoint.tot is not None: if waypoint.tot is not None:
time = timedelta(seconds=int(waypoint.tot.total_seconds())) return f"{waypoint.tot.time()} "
return f"T+{time} "
elif waypoint.departure_time is not None: elif waypoint.departure_time is not None:
time = timedelta(seconds=int(waypoint.departure_time.total_seconds())) return f"{depart_prefix} {waypoint.departure_time.time()} "
return f"{depart_prefix} T+{time} "
return "" return ""

View File

@ -262,11 +262,11 @@ class FlightPlanBuilder:
] ]
) )
def _format_time(self, time: Optional[datetime.timedelta]) -> str: @staticmethod
def _format_time(time: datetime.datetime | None) -> str:
if time is None: if time is None:
return "" return ""
local_time = self.start_time + time return f"{time.strftime('%H:%M:%S')}{'Z' if time.tzinfo is not None else ''}"
return f"{local_time.strftime('%H:%M:%S')}{'Z' if local_time.tzinfo is not None else ''}"
def _format_alt(self, alt: Distance) -> str: def _format_alt(self, alt: Distance) -> str:
return f"{self.units.distance_short(alt):.0f}" return f"{self.units.distance_short(alt):.0f}"
@ -643,11 +643,11 @@ class SupportPage(KneeboardPage):
) )
return f"{channel_name}\n{frequency}" return f"{channel_name}\n{frequency}"
def _format_time(self, time: Optional[datetime.timedelta]) -> str: @staticmethod
def _format_time(time: datetime.datetime | None) -> str:
if time is None: if time is None:
return "" return ""
local_time = self.start_time + time return f"{time.strftime('%H:%M:%S')}{'Z' if time.tzinfo is not None else ''}"
return f"{local_time.strftime('%H:%M:%S')}{'Z' if local_time.tzinfo is not None else ''}"
@staticmethod @staticmethod
def _format_duration(time: Optional[datetime.timedelta]) -> str: def _format_duration(time: Optional[datetime.timedelta]) -> str:

View File

@ -1,7 +1,7 @@
from __future__ import annotations from __future__ import annotations
from dataclasses import dataclass, field from dataclasses import dataclass, field
from datetime import timedelta from datetime import datetime
from typing import Optional, TYPE_CHECKING from typing import Optional, TYPE_CHECKING
from game.dcs.aircrafttype import AircraftType from game.dcs.aircrafttype import AircraftType
@ -33,8 +33,8 @@ class AwacsInfo(GroupInfo):
"""AWACS information for the kneeboard.""" """AWACS information for the kneeboard."""
depature_location: Optional[str] depature_location: Optional[str]
start_time: timedelta start_time: datetime | None
end_time: timedelta end_time: datetime | None
@dataclass @dataclass
@ -43,8 +43,8 @@ class TankerInfo(GroupInfo):
variant: str variant: str
tacan: Optional[TacanChannel] tacan: Optional[TacanChannel]
start_time: timedelta start_time: datetime | None
end_time: timedelta end_time: datetime | None
@dataclass @dataclass

View File

@ -1,7 +1,5 @@
from __future__ import annotations from __future__ import annotations
from datetime import timedelta
from pydantic import BaseModel from pydantic import BaseModel
from game.ato import Flight, FlightWaypoint from game.ato import Flight, FlightWaypoint
@ -21,7 +19,7 @@ def timing_info(flight: Flight, waypoint_idx: int) -> str:
time = flight.flight_plan.depart_time_for_waypoint(waypoint) time = flight.flight_plan.depart_time_for_waypoint(waypoint)
if time is None: if time is None:
return "" return ""
return f"{prefix} T+{timedelta(seconds=int(time.total_seconds()))}" return f"{prefix} {time}"
class FlightWaypointJs(BaseModel): class FlightWaypointJs(BaseModel):

View File

@ -74,11 +74,11 @@ class AircraftSimulation:
now = self.game.conditions.start_time now = self.game.conditions.start_time
for flight in self.iter_flights(): for flight in self.iter_flights():
start_time = flight.flight_plan.startup_time() start_time = flight.flight_plan.startup_time()
if start_time <= timedelta(): if start_time <= now:
self.set_active_flight_state(flight, now) self.set_active_flight_state(flight, now)
else: else:
flight.set_state( flight.set_state(
WaitingForStart(flight, self.game.settings, now + start_time) WaitingForStart(flight, self.game.settings, start_time)
) )
def set_active_flight_state(self, flight: Flight, now: datetime) -> None: def set_active_flight_state(self, flight: Flight, now: datetime) -> None:

View File

@ -4,7 +4,9 @@ import logging
import random import random
from collections.abc import Iterable from collections.abc import Iterable
from dataclasses import dataclass, field from dataclasses import dataclass, field
from typing import Optional, Sequence, TYPE_CHECKING, Any, Union from datetime import datetime
from typing import Any, Union
from typing import Optional, Sequence, TYPE_CHECKING
from uuid import uuid4, UUID from uuid import uuid4, UUID
from dcs.country import Country from dcs.country import Country
@ -380,9 +382,8 @@ class Squadron:
def arrival(self) -> ControlPoint: def arrival(self) -> ControlPoint:
return self.location if self.destination is None else self.destination return self.location if self.destination is None else self.destination
def plan_relocation(self, destination: ControlPoint) -> None: def plan_relocation(self, destination: ControlPoint, now: datetime) -> None:
from game.theater import ParkingType from game.theater import ParkingType
if destination == self.location: if destination == self.location:
logging.warning( logging.warning(
f"Attempted to plan relocation of {self} to current location " f"Attempted to plan relocation of {self} to current location "
@ -402,7 +403,7 @@ class Squadron:
if not destination.can_operate(self.aircraft): if not destination.can_operate(self.aircraft):
raise RuntimeError(f"{self} cannot operate at {destination}.") raise RuntimeError(f"{self} cannot operate at {destination}.")
self.destination = destination self.destination = destination
self.replan_ferry_flights() self.replan_ferry_flights(now)
def cancel_relocation(self) -> None: def cancel_relocation(self) -> None:
from game.theater import ParkingType from game.theater import ParkingType
@ -420,9 +421,9 @@ class Squadron:
self.destination = None self.destination = None
self.cancel_ferry_flights() self.cancel_ferry_flights()
def replan_ferry_flights(self) -> None: def replan_ferry_flights(self, now: datetime) -> None:
self.cancel_ferry_flights() self.cancel_ferry_flights()
self.plan_ferry_flights() self.plan_ferry_flights(now)
def cancel_ferry_flights(self) -> None: def cancel_ferry_flights(self) -> None:
for package in self.coalition.ato.packages: for package in self.coalition.ato.packages:
@ -433,7 +434,7 @@ class Squadron:
if not package.flights: if not package.flights:
self.coalition.ato.remove_package(package) self.coalition.ato.remove_package(package)
def plan_ferry_flights(self) -> None: def plan_ferry_flights(self, now: datetime) -> None:
if self.destination is None: if self.destination is None:
raise RuntimeError( raise RuntimeError(
f"Cannot plan ferry flights for {self} because there is no destination." f"Cannot plan ferry flights for {self} because there is no destination."
@ -447,7 +448,7 @@ class Squadron:
size = min(remaining, self.aircraft.max_group_size) size = min(remaining, self.aircraft.max_group_size)
self.plan_ferry_flight(package, size) self.plan_ferry_flight(package, size)
remaining -= size remaining -= size
package.set_tot_asap() package.set_tot_asap(now)
self.coalition.ato.add_package(package) self.coalition.ato.add_package(package)
def plan_ferry_flight(self, package: Package, size: int) -> None: def plan_ferry_flight(self, package: Package, size: int) -> None:

View File

@ -1011,7 +1011,11 @@ class ControlPoint(MissionTarget, SidcDescribable, ABC):
self.runway_status.begin_repair() self.runway_status.begin_repair()
def process_turn(self, game: Game) -> None: def process_turn(self, game: Game) -> None:
self.ground_unit_orders.process(game) # We're running at the end of the turn, so the time right now is irrelevant, and
# we don't know what time the next turn will start yet. It doesn't actually
# matter though, because the first thing the start of turn action will do is
# clear the ATO and replan the airlifts with the correct time.
self.ground_unit_orders.process(game, game.conditions.start_time)
runway_status = self.runway_status runway_status = self.runway_status
if runway_status is not None: if runway_status is not None:

View File

@ -35,6 +35,7 @@ import logging
import math import math
from collections import defaultdict from collections import defaultdict
from dataclasses import dataclass, field from dataclasses import dataclass, field
from datetime import datetime
from functools import singledispatchmethod from functools import singledispatchmethod
from typing import Generic, Iterator, List, Optional, Sequence, TYPE_CHECKING, TypeVar from typing import Generic, Iterator, List, Optional, Sequence, TYPE_CHECKING, TypeVar
@ -299,7 +300,7 @@ class AirliftPlanner:
return True return True
def create_package_for_airlift(self) -> None: def create_package_for_airlift(self, now: datetime) -> None:
distance_cache = ObjectiveDistanceCache.get_closest_airfields( distance_cache = ObjectiveDistanceCache.get_closest_airfields(
self.transfer.position self.transfer.position
) )
@ -318,7 +319,7 @@ class AirliftPlanner:
): ):
self.create_airlift_flight(squadron) self.create_airlift_flight(squadron)
if self.package.flights: if self.package.flights:
self.package.set_tot_asap() self.package.set_tot_asap(now)
self.game.ato_for(self.for_player).add_package(self.package) self.game.ato_for(self.for_player).add_package(self.package)
def create_airlift_flight(self, squadron: Squadron) -> int: def create_airlift_flight(self, squadron: Squadron) -> int:
@ -590,7 +591,7 @@ class PendingTransfers:
def network_for(self, control_point: ControlPoint) -> TransitNetwork: def network_for(self, control_point: ControlPoint) -> TransitNetwork:
return self.game.transit_network_for(control_point.captured) return self.game.transit_network_for(control_point.captured)
def arrange_transport(self, transfer: TransferOrder) -> None: def arrange_transport(self, transfer: TransferOrder, now: datetime) -> None:
network = self.network_for(transfer.position) network = self.network_for(transfer.position)
path = network.shortest_path_between(transfer.position, transfer.destination) path = network.shortest_path_between(transfer.position, transfer.destination)
next_stop = path[0] next_stop = path[0]
@ -605,12 +606,12 @@ class PendingTransfers:
== TransitConnection.Shipping == TransitConnection.Shipping
): ):
return self.cargo_ships.add(transfer, next_stop) return self.cargo_ships.add(transfer, next_stop)
AirliftPlanner(self.game, transfer, next_stop).create_package_for_airlift() AirliftPlanner(self.game, transfer, next_stop).create_package_for_airlift(now)
def new_transfer(self, transfer: TransferOrder) -> None: def new_transfer(self, transfer: TransferOrder, now: datetime) -> None:
transfer.origin.base.commit_losses(transfer.units) transfer.origin.base.commit_losses(transfer.units)
self.pending_transfers.append(transfer) self.pending_transfers.append(transfer)
self.arrange_transport(transfer) self.arrange_transport(transfer, now)
self._send_supply_route_event_stream_update() self._send_supply_route_event_stream_update()
def split_transfer(self, transfer: TransferOrder, size: int) -> TransferOrder: def split_transfer(self, transfer: TransferOrder, size: int) -> TransferOrder:
@ -692,7 +693,7 @@ class PendingTransfers:
self.cargo_ships.disband_all() self.cargo_ships.disband_all()
self._send_supply_route_event_stream_update() self._send_supply_route_event_stream_update()
def plan_transports(self) -> None: def plan_transports(self, now: datetime) -> None:
""" """
Plan transports for all pending and completable transfers which don't have a Plan transports for all pending and completable transfers which don't have a
transport assigned already. This calculates the shortest path between current transport assigned already. This calculates the shortest path between current
@ -702,7 +703,7 @@ class PendingTransfers:
self.disband_uncompletable_transfers() self.disband_uncompletable_transfers()
for transfer in self.pending_transfers: for transfer in self.pending_transfers:
if transfer.transport is None: if transfer.transport is None:
self.arrange_transport(transfer) self.arrange_transport(transfer, now)
def disband_uncompletable_transfers(self) -> None: def disband_uncompletable_transfers(self) -> None:
""" """

View File

@ -142,11 +142,9 @@ class PackageModel(QAbstractListModel):
@staticmethod @staticmethod
def text_for_flight(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."""
delay = datetime.timedelta(
seconds=int(flight.flight_plan.startup_time().total_seconds())
)
origin = flight.departure.name origin = flight.departure.name
return f"{flight} from {origin} in {delay}" startup = flight.flight_plan.startup_time()
return f"{flight} from {origin} at {startup}"
@staticmethod @staticmethod
def icon_for_flight(flight: Flight) -> Optional[QIcon]: def icon_for_flight(flight: Flight) -> Optional[QIcon]:
@ -191,7 +189,7 @@ class PackageModel(QAbstractListModel):
"""Returns the flight located at the given index.""" """Returns the flight located at the given index."""
return self.package.flights[index.row()] return self.package.flights[index.row()]
def set_tot(self, tot: datetime.timedelta) -> None: def set_tot(self, tot: datetime.datetime) -> None:
self.package.time_over_target = tot self.package.time_over_target = tot
self.update_tot() self.update_tot()
@ -201,7 +199,9 @@ class PackageModel(QAbstractListModel):
def update_tot(self) -> None: def update_tot(self) -> None:
if self.package.auto_asap: if self.package.auto_asap:
self.package.set_tot_asap() self.package.set_tot_asap(
self.game_model.sim_controller.current_time_in_sim
)
self.tot_changed.emit() self.tot_changed.emit()
# For some reason this is needed to make the UI update quickly. # For some reason this is needed to make the UI update quickly.
self.layoutChanged.emit() self.layoutChanged.emit()
@ -395,11 +395,11 @@ class TransferModel(QAbstractListModel):
"""Returns the icon that should be displayed for the transfer.""" """Returns the icon that should be displayed for the transfer."""
return None return None
def new_transfer(self, transfer: TransferOrder) -> None: def new_transfer(self, transfer: TransferOrder, now: datetime) -> None:
"""Updates the game with the new unit transfer.""" """Updates the game with the new unit transfer."""
self.beginInsertRows(QModelIndex(), self.rowCount(), self.rowCount()) self.beginInsertRows(QModelIndex(), self.rowCount(), self.rowCount())
# TODO: Needs to regenerate base inventory tab. # TODO: Needs to regenerate base inventory tab.
self.transfers.new_transfer(transfer) self.transfers.new_transfer(transfer, now)
self.endInsertRows() self.endInsertRows()
def cancel_transfer_at_index(self, index: QModelIndex) -> None: def cancel_transfer_at_index(self, index: QModelIndex) -> None:

View File

@ -34,11 +34,18 @@ class SimController(QObject):
return self.game_loop.completed return self.game_loop.completed
@property @property
def current_time_in_sim(self) -> Optional[datetime]: def current_time_in_sim_if_game_loaded(self) -> datetime | None:
if self.game_loop is None: if self.game_loop is None:
return None return None
return self.game_loop.current_time_in_sim return self.game_loop.current_time_in_sim
@property
def current_time_in_sim(self) -> datetime:
time = self.current_time_in_sim_if_game_loaded
if time is None:
raise RuntimeError("No game is loaded")
return time
@property @property
def elapsed_time(self) -> timedelta: def elapsed_time(self) -> timedelta:
if self.game_loop is None: if self.game_loop is None:

View File

@ -58,7 +58,7 @@ class QTimeTurnWidget(QGroupBox):
sim_controller.sim_update.connect(self.on_sim_update) sim_controller.sim_update.connect(self.on_sim_update)
def on_sim_update(self, _events: GameUpdateEvents) -> None: def on_sim_update(self, _events: GameUpdateEvents) -> None:
time = self.sim_controller.current_time_in_sim time = self.sim_controller.current_time_in_sim_if_game_loaded
if time is None: if time is None:
self.date_display.setText("") self.date_display.setText("")
self.time_display.setText("") self.time_display.setText("")

View File

@ -1,3 +1,4 @@
from datetime import datetime
from typing import List, Optional from typing import List, Optional
from PySide2.QtWidgets import ( from PySide2.QtWidgets import (
@ -166,7 +167,7 @@ class QTopPanel(QFrame):
GameUpdateSignal.get_instance().updateGame(self.game) GameUpdateSignal.get_instance().updateGame(self.game)
self.proceedButton.setEnabled(True) self.proceedButton.setEnabled(True)
def negative_start_packages(self) -> List[Package]: def negative_start_packages(self, now: datetime) -> List[Package]:
packages = [] packages = []
for package in self.game_model.ato_model.ato.packages: for package in self.game_model.ato_model.ato.packages:
if not package.flights: if not package.flights:
@ -174,7 +175,7 @@ class QTopPanel(QFrame):
for flight in package.flights: for flight in package.flights:
if flight.state.is_waiting_for_start: if flight.state.is_waiting_for_start:
startup = flight.flight_plan.startup_time() startup = flight.flight_plan.startup_time()
if startup < 0: if startup < now:
packages.append(package) packages.append(package)
break break
return packages return packages
@ -290,7 +291,9 @@ class QTopPanel(QFrame):
if self.check_no_missing_pilots(): if self.check_no_missing_pilots():
return return
negative_starts = self.negative_start_packages() negative_starts = self.negative_start_packages(
self.sim_controller.current_time_in_sim
)
if negative_starts: if negative_starts:
if not self.confirm_negative_start_time(negative_starts): if not self.confirm_negative_start_time(negative_starts):
return return

View File

@ -1,6 +1,5 @@
"""Widgets for displaying air tasking orders.""" """Widgets for displaying air tasking orders."""
import logging import logging
from datetime import timedelta
from typing import Optional from typing import Optional
from PySide2.QtCore import ( from PySide2.QtCore import (
@ -285,16 +284,7 @@ class PackageDelegate(TwoColumnRowDelegate):
clients = self.num_clients(index) clients = self.num_clients(index)
return f"Player Slots: {clients}" if clients else "" return f"Player Slots: {clients}" if clients else ""
elif (row, column) == (1, 0): elif (row, column) == (1, 0):
tot_delay = ( return f"TOT at {package.time_over_target:%H:%M:%S}"
package.time_over_target - self.game_model.sim_controller.elapsed_time
)
if tot_delay >= timedelta():
return f"TOT in {tot_delay}"
game = self.game_model.game
if game is None:
raise RuntimeError("Package TOT has elapsed but no game is loaded")
tot_time = game.conditions.start_time + package.time_over_target
return f"TOT passed at {tot_time:%H:%M:%S}"
elif (row, column) == (1, 1): elif (row, column) == (1, 1):
unassigned_pilots = self.missing_pilots(index) unassigned_pilots = self.missing_pilots(index)
return f"Missing pilots: {unassigned_pilots}" if unassigned_pilots else "" return f"Missing pilots: {unassigned_pilots}" if unassigned_pilots else ""

View File

@ -1,27 +1,28 @@
from __future__ import annotations from __future__ import annotations
from dataclasses import dataclass from dataclasses import dataclass
from typing import Optional, Iterator from typing import Iterator, Optional
from PySide2.QtCore import QItemSelectionModel, QModelIndex, QSize from PySide2.QtCore import QItemSelectionModel, QModelIndex, QSize
from PySide2.QtWidgets import ( from PySide2.QtWidgets import (
QAbstractItemView, QAbstractItemView,
QCheckBox, QCheckBox,
QDialog, QDialog,
QHBoxLayout,
QListView, QListView,
QVBoxLayout,
QTabWidget, QTabWidget,
QTableWidget, QTableWidget,
QTableWidgetItem, QTableWidgetItem,
QVBoxLayout,
QWidget, QWidget,
QHBoxLayout,
) )
from game.ato.flight import Flight from game.ato.flight import Flight
from game.squadrons import Squadron from game.squadrons import Squadron
from game.theater import ConflictTheater from game.theater import ConflictTheater
from qt_ui.delegates import TwoColumnRowDelegate from qt_ui.delegates import TwoColumnRowDelegate
from qt_ui.models import GameModel, AirWingModel, SquadronModel, AtoModel from qt_ui.models import AirWingModel, AtoModel, GameModel, SquadronModel
from qt_ui.simcontroller import SimController
from qt_ui.windows.SquadronDialog import SquadronDialog from qt_ui.windows.SquadronDialog import SquadronDialog
@ -63,11 +64,13 @@ class SquadronList(QListView):
ato_model: AtoModel, ato_model: AtoModel,
air_wing_model: AirWingModel, air_wing_model: AirWingModel,
theater: ConflictTheater, theater: ConflictTheater,
sim_controller: SimController,
) -> None: ) -> None:
super().__init__() super().__init__()
self.ato_model = ato_model self.ato_model = ato_model
self.air_wing_model = air_wing_model self.air_wing_model = air_wing_model
self.theater = theater self.theater = theater
self.sim_controller = sim_controller
self.dialog: Optional[SquadronDialog] = None self.dialog: Optional[SquadronDialog] = None
self.setIconSize(QSize(91, 24)) self.setIconSize(QSize(91, 24))
@ -88,6 +91,7 @@ class SquadronList(QListView):
self.ato_model, self.ato_model,
SquadronModel(self.air_wing_model.squadron_at_index(index)), SquadronModel(self.air_wing_model.squadron_at_index(index)),
self.theater, self.theater,
self.sim_controller,
self, self,
) )
self.dialog.show() self.dialog.show()
@ -234,6 +238,7 @@ class AirWingTabs(QTabWidget):
game_model.ato_model, game_model.ato_model,
game_model.blue_air_wing_model, game_model.blue_air_wing_model,
game_model.game.theater, game_model.game.theater,
game_model.sim_controller,
), ),
"Squadrons", "Squadrons",
) )

View File

@ -30,6 +30,7 @@ from game.theater import ConflictTheater, ControlPoint, ParkingType
from qt_ui.delegates import TwoColumnRowDelegate from qt_ui.delegates import TwoColumnRowDelegate
from qt_ui.errorreporter import report_errors from qt_ui.errorreporter import report_errors
from qt_ui.models import AtoModel, SquadronModel from qt_ui.models import AtoModel, SquadronModel
from qt_ui.simcontroller import SimController
from qt_ui.widgets.combos.QSquadronLiverySelector import SquadronLiverySelector from qt_ui.widgets.combos.QSquadronLiverySelector import SquadronLiverySelector
from qt_ui.widgets.combos.primarytaskselector import PrimaryTaskSelector from qt_ui.widgets.combos.primarytaskselector import PrimaryTaskSelector
@ -230,11 +231,13 @@ class SquadronDialog(QDialog):
ato_model: AtoModel, ato_model: AtoModel,
squadron_model: SquadronModel, squadron_model: SquadronModel,
theater: ConflictTheater, theater: ConflictTheater,
sim_controller: SimController,
parent, parent,
) -> None: ) -> None:
super().__init__(parent) super().__init__(parent)
self.ato_model = ato_model self.ato_model = ato_model
self.squadron_model = squadron_model self.squadron_model = squadron_model
self.sim_controller = sim_controller
self.setMinimumSize(1000, 440) self.setMinimumSize(1000, 440)
self.setWindowTitle(str(squadron_model.squadron)) self.setWindowTitle(str(squadron_model.squadron))
@ -327,7 +330,9 @@ class SquadronDialog(QDialog):
elif self.ato_model.game.settings.enable_transfer_cheat: elif self.ato_model.game.settings.enable_transfer_cheat:
self._instant_relocate(destination) self._instant_relocate(destination)
else: else:
self.squadron.plan_relocation(destination) self.squadron.plan_relocation(
destination, self.sim_controller.current_time_in_sim
)
self.ato_model.replace_from_game(player=True) self.ato_model.replace_from_game(player=True)
def check_disabled_button_states( def check_disabled_button_states(

View File

@ -320,7 +320,9 @@ class NewUnitTransferDialog(QDialog):
units=transfers, units=transfers,
request_airflift=self.dest_panel.request_airlift, request_airflift=self.dest_panel.request_airlift,
) )
self.game_model.transfer_model.new_transfer(transfer) self.game_model.transfer_model.new_transfer(
transfer, self.game_model.sim_controller.current_time_in_sim
)
self.close() self.close()
def on_transfer_quantity_changed(self) -> None: def on_transfer_quantity_changed(self) -> None:

View File

@ -19,4 +19,4 @@ class QFlightItem(QStandardItem):
) )
self.setIcon(icon) self.setIcon(icon)
self.setEditable(False) self.setEditable(False)
self.setText(f"{flight} in {flight.flight_plan.startup_time()}") self.setText(f"{flight} at {flight.flight_plan.startup_time()}")

View File

@ -1,6 +1,5 @@
"""Dialogs for creating and editing ATO packages.""" """Dialogs for creating and editing ATO packages."""
import logging import logging
from datetime import timedelta
from typing import Optional from typing import Optional
from PySide2.QtCore import QItemSelection, QTime, Qt, Signal from PySide2.QtCore import QItemSelection, QTime, Qt, Signal
@ -93,7 +92,7 @@ class QPackageDialog(QDialog):
self.tot_spinner = QTimeEdit(self.tot_qtime()) self.tot_spinner = QTimeEdit(self.tot_qtime())
self.tot_spinner.setMinimumTime(QTime(0, 0)) self.tot_spinner.setMinimumTime(QTime(0, 0))
self.tot_spinner.setDisplayFormat("T+hh:mm:ss") self.tot_spinner.setDisplayFormat("hh:mm:ss")
self.tot_spinner.timeChanged.connect(self.save_tot) self.tot_spinner.timeChanged.connect(self.save_tot)
self.tot_spinner.setToolTip("Package TOT relative to mission TOT") self.tot_spinner.setToolTip("Package TOT relative to mission TOT")
self.tot_spinner.setEnabled(not self.package_model.package.auto_asap) self.tot_spinner.setEnabled(not self.package_model.package.auto_asap)
@ -159,11 +158,8 @@ class QPackageDialog(QDialog):
return self.game_model.game return self.game_model.game
def tot_qtime(self) -> QTime: def tot_qtime(self) -> QTime:
delay = int(self.package_model.package.time_over_target.total_seconds()) tot = self.package_model.package.time_over_target
hours = delay // 3600 return QTime(tot.hour, tot.minute, tot.second)
minutes = delay // 60 % 60
seconds = delay % 60
return QTime(hours, minutes, seconds)
def on_cancel(self) -> None: def on_cancel(self) -> None:
pass pass
@ -177,9 +173,13 @@ class QPackageDialog(QDialog):
self.save_tot() self.save_tot()
def save_tot(self) -> None: def save_tot(self) -> None:
# TODO: This is going to break horribly around midnight.
time = self.tot_spinner.time() time = self.tot_spinner.time()
seconds = time.hour() * 3600 + time.minute() * 60 + time.second() self.package_model.set_tot(
self.package_model.set_tot(timedelta(seconds=seconds)) self.package_model.package.time_over_target.replace(
hour=time.hour(), minute=time.minute(), second=time.second()
)
)
def set_asap(self, checked: bool) -> None: def set_asap(self, checked: bool) -> None:
self.package_model.set_asap(checked) self.package_model.set_asap(checked)

View File

@ -1,4 +1,4 @@
from datetime import timedelta from datetime import datetime
from PySide2.QtCore import QItemSelectionModel, QSize from PySide2.QtCore import QItemSelectionModel, QSize
from PySide2.QtGui import QStandardItemModel from PySide2.QtGui import QStandardItemModel
@ -50,5 +50,5 @@ class QPlannedFlightsView(QListView):
self.setup_content() self.setup_content()
@staticmethod @staticmethod
def mission_start_for_flight(flight_item: QFlightItem) -> timedelta: def mission_start_for_flight(flight_item: QFlightItem) -> datetime:
return flight_item.flight.flight_plan.startup_time() return flight_item.flight.flight_plan.startup_time()

View File

@ -102,7 +102,9 @@ class FlightPlanPropertiesGroup(QGroupBox):
# handler may be called for a flight whose package has been canceled, which # handler may be called for a flight whose package has been canceled, which
# is an invalid state for calling anything in TotEstimator. # is an invalid state for calling anything in TotEstimator.
return return
self.departure_time.setText(f"At T+{self.flight.flight_plan.startup_time()}") self.departure_time.setText(
f"At {self.flight.flight_plan.startup_time():%H:%M%S}"
)
self.flight_wpt_list.update_list() self.flight_wpt_list.update_list()
def set_divert(self, index: int) -> None: def set_divert(self, index: int) -> None:

View File

@ -1,5 +1,3 @@
from datetime import timedelta
from PySide2.QtCore import QItemSelectionModel, QPoint, QModelIndex from PySide2.QtCore import QItemSelectionModel, QPoint, QModelIndex
from PySide2.QtGui import QStandardItem, QStandardItemModel from PySide2.QtGui import QStandardItem, QStandardItemModel
from PySide2.QtWidgets import ( from PySide2.QtWidgets import (
@ -120,14 +118,8 @@ class QFlightWaypointList(QTableView):
time = flight.flight_plan.depart_time_for_waypoint(waypoint) time = flight.flight_plan.depart_time_for_waypoint(waypoint)
if time is None: if time is None:
return "" return ""
time = timedelta(seconds=int(time.total_seconds())) return f"{prefix}{time:%H:%M:%S}"
return f"{prefix}T+{time}"
@staticmethod @staticmethod
def takeoff_text(flight: Flight) -> str: def takeoff_text(flight: Flight) -> str:
takeoff_time = flight.flight_plan.takeoff_time() return f"{flight.flight_plan.takeoff_time():%H:%M:%S}"
# Handle custom flight plans where we can't estimate the takeoff time.
if takeoff_time is None:
takeoff_time = timedelta()
start_time = timedelta(seconds=int(takeoff_time.total_seconds()))
return f"T+{start_time}"