mirror of
https://github.com/dcs-retribution/dcs-retribution.git
synced 2025-11-10 15:41:24 +00:00
Convert TOTs to datetime.
https://github.com/dcs-liberation/dcs_liberation/issues/1680
This commit is contained in:
parent
625c4f2dfb
commit
59673e7911
@ -1,7 +1,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from datetime import timedelta
|
||||
from datetime import datetime
|
||||
from typing import Iterator, TYPE_CHECKING, Type
|
||||
|
||||
from game.theater.controlpoint import ControlPointType
|
||||
@ -65,21 +65,21 @@ class AirAssaultFlightPlan(FormationAttackFlightPlan, UiZoneDisplay):
|
||||
return self.layout.targets[0]
|
||||
|
||||
@property
|
||||
def ingress_time(self) -> timedelta:
|
||||
def ingress_time(self) -> datetime:
|
||||
tot = self.tot
|
||||
travel_time = self.travel_time_between_waypoints(
|
||||
self.layout.ingress, self.tot_waypoint
|
||||
)
|
||||
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:
|
||||
return self.tot
|
||||
elif waypoint is self.layout.ingress:
|
||||
return self.ingress_time
|
||||
return None
|
||||
|
||||
def depart_time_for_waypoint(self, waypoint: FlightWaypoint) -> timedelta | None:
|
||||
def depart_time_for_waypoint(self, waypoint: FlightWaypoint) -> datetime | None:
|
||||
return None
|
||||
|
||||
@property
|
||||
@ -87,7 +87,11 @@ class AirAssaultFlightPlan(FormationAttackFlightPlan, UiZoneDisplay):
|
||||
return meters(2500)
|
||||
|
||||
@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
|
||||
|
||||
def ui_zone(self) -> UiZone:
|
||||
|
||||
@ -2,8 +2,9 @@ from __future__ import annotations
|
||||
|
||||
from collections.abc import Iterator
|
||||
from dataclasses import dataclass
|
||||
from datetime import timedelta
|
||||
from typing import TYPE_CHECKING, Type, Optional
|
||||
from datetime import datetime
|
||||
from typing import Optional
|
||||
from typing import TYPE_CHECKING, Type
|
||||
|
||||
from game.theater.missiontarget import MissionTarget
|
||||
from game.utils import feet
|
||||
@ -89,16 +90,20 @@ class AirliftFlightPlan(StandardFlightPlan[AirliftLayout]):
|
||||
# drop-off waypoint.
|
||||
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
|
||||
# lines so no need to wait for escorts or for other missions to complete.
|
||||
return None
|
||||
|
||||
def depart_time_for_waypoint(self, waypoint: FlightWaypoint) -> timedelta | None:
|
||||
def depart_time_for_waypoint(self, waypoint: FlightWaypoint) -> datetime | None:
|
||||
return None
|
||||
|
||||
@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
|
||||
|
||||
|
||||
|
||||
@ -2,7 +2,7 @@ from __future__ import annotations
|
||||
|
||||
from collections.abc import Iterator
|
||||
from dataclasses import dataclass
|
||||
from datetime import timedelta
|
||||
from datetime import datetime
|
||||
from typing import TYPE_CHECKING, Type
|
||||
|
||||
from .flightplan import FlightPlan, Layout
|
||||
@ -46,16 +46,20 @@ class CustomFlightPlan(FlightPlan[CustomLayout]):
|
||||
return waypoint
|
||||
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:
|
||||
return self.package.time_over_target + self.tot_offset
|
||||
return None
|
||||
|
||||
def depart_time_for_waypoint(self, waypoint: FlightWaypoint) -> timedelta | None:
|
||||
def depart_time_for_waypoint(self, waypoint: FlightWaypoint) -> datetime | None:
|
||||
return None
|
||||
|
||||
@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
|
||||
|
||||
|
||||
|
||||
@ -2,7 +2,7 @@ from __future__ import annotations
|
||||
|
||||
from collections.abc import Iterator
|
||||
from dataclasses import dataclass
|
||||
from datetime import timedelta
|
||||
from datetime import datetime
|
||||
from typing import TYPE_CHECKING, Type
|
||||
|
||||
from game.utils import feet
|
||||
@ -35,16 +35,20 @@ class FerryFlightPlan(StandardFlightPlan[FerryLayout]):
|
||||
def tot_waypoint(self) -> FlightWaypoint:
|
||||
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
|
||||
# lines so no need to wait for escorts or for other missions to complete.
|
||||
return None
|
||||
|
||||
def depart_time_for_waypoint(self, waypoint: FlightWaypoint) -> timedelta | None:
|
||||
def depart_time_for_waypoint(self, waypoint: FlightWaypoint) -> datetime | None:
|
||||
return None
|
||||
|
||||
@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
|
||||
|
||||
|
||||
|
||||
@ -8,10 +8,10 @@ generating the waypoints for the mission.
|
||||
from __future__ import annotations
|
||||
|
||||
import math
|
||||
from abc import ABC
|
||||
from abc import ABC, abstractmethod
|
||||
from collections.abc import Iterator
|
||||
from dataclasses import dataclass
|
||||
from datetime import timedelta
|
||||
from datetime import datetime, timedelta
|
||||
from functools import cached_property
|
||||
from typing import Any, Generic, TYPE_CHECKING, TypeGuard, TypeVar
|
||||
|
||||
@ -159,7 +159,7 @@ class FlightPlan(ABC, Generic[LayoutT]):
|
||||
raise NotImplementedError
|
||||
|
||||
@property
|
||||
def tot(self) -> timedelta:
|
||||
def tot(self) -> datetime:
|
||||
return self.package.time_over_target + self.tot_offset
|
||||
|
||||
@cached_property
|
||||
@ -224,6 +224,8 @@ class FlightPlan(ABC, Generic[LayoutT]):
|
||||
|
||||
for previous_waypoint, waypoint in self.edges(until=destination):
|
||||
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
|
||||
# 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
|
||||
@ -249,10 +251,10 @@ class FlightPlan(ABC, Generic[LayoutT]):
|
||||
distance = meters(a.position.distance_to_point(b.position))
|
||||
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
|
||||
|
||||
def depart_time_for_waypoint(self, waypoint: FlightWaypoint) -> timedelta | None:
|
||||
def depart_time_for_waypoint(self, waypoint: FlightWaypoint) -> datetime | None:
|
||||
raise NotImplementedError
|
||||
|
||||
def request_escort_at(self) -> FlightWaypoint | None:
|
||||
@ -275,12 +277,14 @@ class FlightPlan(ABC, Generic[LayoutT]):
|
||||
if waypoint == end:
|
||||
return
|
||||
|
||||
def takeoff_time(self) -> timedelta:
|
||||
def takeoff_time(self) -> datetime:
|
||||
return self.tot - self._travel_time_to_waypoint(self.tot_waypoint)
|
||||
|
||||
def startup_time(self) -> timedelta:
|
||||
start_time = (
|
||||
self.takeoff_time() - self.estimate_startup() - self.estimate_ground_ops()
|
||||
def minimum_duration_from_start_to_tot(self) -> timedelta:
|
||||
return (
|
||||
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
|
||||
@ -304,6 +308,11 @@ class FlightPlan(ABC, Generic[LayoutT]):
|
||||
|
||||
return start_time
|
||||
|
||||
def startup_time(self) -> datetime:
|
||||
return (
|
||||
self.takeoff_time() - self.estimate_startup() - self.estimate_ground_ops()
|
||||
)
|
||||
|
||||
def estimate_startup(self) -> timedelta:
|
||||
if self.flight.start_type is StartType.COLD:
|
||||
if self.flight.client_count:
|
||||
@ -325,12 +334,16 @@ class FlightPlan(ABC, Generic[LayoutT]):
|
||||
def is_airassault(self) -> bool:
|
||||
return False
|
||||
|
||||
@abstractmethod
|
||||
def mission_begin_on_station_time(self) -> datetime | None:
|
||||
"""The time that the mission is first on-station."""
|
||||
|
||||
@property
|
||||
def is_custom(self) -> bool:
|
||||
return False
|
||||
|
||||
@property
|
||||
def mission_departure_time(self) -> timedelta:
|
||||
def mission_departure_time(self) -> datetime:
|
||||
"""The time that the mission is complete and the flight RTBs."""
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
@ -2,7 +2,7 @@ from __future__ import annotations
|
||||
|
||||
from abc import ABC, abstractmethod
|
||||
from dataclasses import dataclass
|
||||
from datetime import timedelta
|
||||
from datetime import datetime, timedelta
|
||||
from functools import cached_property
|
||||
from typing import Any, TYPE_CHECKING, TypeGuard, Optional
|
||||
|
||||
@ -75,15 +75,15 @@ class FormationFlightPlan(LoiterFlightPlan, ABC):
|
||||
|
||||
@property
|
||||
@abstractmethod
|
||||
def join_time(self) -> timedelta:
|
||||
def join_time(self) -> datetime:
|
||||
...
|
||||
|
||||
@property
|
||||
@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:
|
||||
return self.join_time + self.tot_offset
|
||||
elif waypoint == self.layout.split:
|
||||
@ -91,13 +91,18 @@ class FormationFlightPlan(LoiterFlightPlan, ABC):
|
||||
return None
|
||||
|
||||
@property
|
||||
def push_time(self) -> timedelta:
|
||||
def push_time(self) -> datetime:
|
||||
return self.join_time - self.travel_time_between_waypoints(
|
||||
self.layout.hold, self.layout.join
|
||||
self.layout.hold.position,
|
||||
self.layout.join.position,
|
||||
)
|
||||
|
||||
@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
|
||||
|
||||
@self_type_guard
|
||||
|
||||
@ -3,8 +3,9 @@ from __future__ import annotations
|
||||
from abc import ABC
|
||||
from collections.abc import Iterator
|
||||
from dataclasses import dataclass
|
||||
from datetime import timedelta
|
||||
from typing import TYPE_CHECKING, TypeVar, Optional
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Optional
|
||||
from typing import TYPE_CHECKING, TypeVar
|
||||
|
||||
from dcs import Point
|
||||
|
||||
@ -14,6 +15,7 @@ from game.utils import Speed, meters, nautical_miles, feet
|
||||
from .flightplan import FlightPlan
|
||||
from .formation import FormationFlightPlan, FormationLayout
|
||||
from .ibuilder import IBuilder
|
||||
from .planningerror import PlanningError
|
||||
from .waypointbuilder import StrikeTarget, WaypointBuilder
|
||||
from .. import FlightType
|
||||
from ..flightwaypoint import FlightWaypoint
|
||||
@ -56,14 +58,37 @@ class FormationAttackFlightPlan(FormationFlightPlan, ABC):
|
||||
)
|
||||
|
||||
@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(
|
||||
self.layout.join, self.layout.ingress
|
||||
)
|
||||
return self.ingress_time - travel_time
|
||||
|
||||
@property
|
||||
def split_time(self) -> timedelta:
|
||||
def split_time(self) -> datetime:
|
||||
travel_time_ingress = self.total_time_between_waypoints(
|
||||
self.layout.ingress, self.target_area_waypoint
|
||||
)
|
||||
@ -80,7 +105,7 @@ class FormationAttackFlightPlan(FormationFlightPlan, ABC):
|
||||
)
|
||||
|
||||
@property
|
||||
def ingress_time(self) -> timedelta:
|
||||
def ingress_time(self) -> datetime:
|
||||
tot = self.tot
|
||||
travel_time = self.total_time_between_waypoints(
|
||||
self.layout.ingress, self.target_area_waypoint
|
||||
@ -88,14 +113,14 @@ class FormationAttackFlightPlan(FormationFlightPlan, ABC):
|
||||
return tot - travel_time
|
||||
|
||||
@property
|
||||
def initial_time(self) -> timedelta:
|
||||
def initial_time(self) -> datetime:
|
||||
tot = self.tot
|
||||
travel_time = self.travel_time_between_waypoints(
|
||||
self.layout.initial, self.target_area_waypoint
|
||||
)
|
||||
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:
|
||||
return self.ingress_time
|
||||
elif waypoint == self.layout.initial:
|
||||
|
||||
@ -2,8 +2,9 @@ from __future__ import annotations
|
||||
|
||||
from abc import ABC, abstractmethod
|
||||
from dataclasses import dataclass
|
||||
from datetime import timedelta
|
||||
from typing import Any, TYPE_CHECKING, TypeGuard, Optional
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Any, TYPE_CHECKING, TypeGuard
|
||||
from typing import Optional
|
||||
|
||||
from game.typeguard import self_type_guard
|
||||
from .flightplan import FlightPlan
|
||||
@ -25,10 +26,10 @@ class LoiterFlightPlan(StandardFlightPlan[Any], ABC):
|
||||
|
||||
@property
|
||||
@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:
|
||||
return self.push_time + self.tot_offset
|
||||
return None
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import timedelta
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Type
|
||||
|
||||
from dcs import Point
|
||||
@ -39,7 +39,7 @@ class PackageRefuelingFlightPlan(RefuelingFlightPlan):
|
||||
)
|
||||
|
||||
@property
|
||||
def patrol_start_time(self) -> timedelta:
|
||||
def patrol_start_time(self) -> datetime:
|
||||
altitude = self.flight.unit_type.patrol_altitude
|
||||
|
||||
if altitude is None:
|
||||
|
||||
@ -3,7 +3,7 @@ from __future__ import annotations
|
||||
from abc import ABC, abstractmethod
|
||||
from collections.abc import Iterator
|
||||
from dataclasses import dataclass
|
||||
from datetime import timedelta
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Any, TYPE_CHECKING, TypeGuard, TypeVar
|
||||
|
||||
from game.ato.flightplans.standard import StandardFlightPlan, StandardLayout
|
||||
@ -59,22 +59,22 @@ class PatrollingFlightPlan(StandardFlightPlan[LayoutT], UiZoneDisplay, ABC):
|
||||
"""
|
||||
|
||||
@property
|
||||
def patrol_start_time(self) -> timedelta:
|
||||
def patrol_start_time(self) -> datetime:
|
||||
return self.tot
|
||||
|
||||
@property
|
||||
def patrol_end_time(self) -> timedelta:
|
||||
def patrol_end_time(self) -> datetime:
|
||||
# TODO: This is currently wrong for CAS.
|
||||
# CAS missions end when they're winchester or bingo. We need to
|
||||
# configure push tasks for the escorts rather than relying on timing.
|
||||
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:
|
||||
return self.patrol_start_time
|
||||
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:
|
||||
return self.patrol_end_time
|
||||
return None
|
||||
@ -88,7 +88,11 @@ class PatrollingFlightPlan(StandardFlightPlan[LayoutT], UiZoneDisplay, ABC):
|
||||
return self.layout.patrol_start
|
||||
|
||||
@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
|
||||
|
||||
@self_type_guard
|
||||
|
||||
@ -2,7 +2,7 @@ from __future__ import annotations
|
||||
|
||||
from collections.abc import Iterator
|
||||
from dataclasses import dataclass
|
||||
from datetime import timedelta
|
||||
from datetime import datetime
|
||||
from typing import TYPE_CHECKING, Type
|
||||
|
||||
from game.utils import feet
|
||||
@ -42,15 +42,19 @@ class RtbFlightPlan(StandardFlightPlan[RtbLayout]):
|
||||
def tot_waypoint(self) -> FlightWaypoint:
|
||||
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
|
||||
|
||||
def depart_time_for_waypoint(self, waypoint: FlightWaypoint) -> timedelta | None:
|
||||
def depart_time_for_waypoint(self, waypoint: FlightWaypoint) -> datetime | None:
|
||||
return None
|
||||
|
||||
@property
|
||||
def mission_departure_time(self) -> timedelta:
|
||||
return timedelta()
|
||||
def mission_begin_on_station_time(self) -> datetime | None:
|
||||
return None
|
||||
|
||||
@property
|
||||
def mission_departure_time(self) -> datetime:
|
||||
return self.tot
|
||||
|
||||
|
||||
class Builder(IBuilder[RtbFlightPlan, RtbLayout]):
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from datetime import timedelta
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Iterator, TYPE_CHECKING, Type
|
||||
|
||||
from dcs import Point
|
||||
@ -54,36 +54,40 @@ class SweepFlightPlan(LoiterFlightPlan):
|
||||
return -timedelta(minutes=5)
|
||||
|
||||
@property
|
||||
def sweep_start_time(self) -> timedelta:
|
||||
def sweep_start_time(self) -> datetime:
|
||||
travel_time = self.total_time_between_waypoints(
|
||||
self.layout.sweep_start, self.layout.sweep_end
|
||||
)
|
||||
return self.sweep_end_time - travel_time
|
||||
|
||||
@property
|
||||
def sweep_end_time(self) -> timedelta:
|
||||
def sweep_end_time(self) -> datetime:
|
||||
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:
|
||||
return self.sweep_start_time + self.tot_offset
|
||||
if waypoint == self.layout.sweep_end:
|
||||
return self.sweep_end_time + self.tot_offset
|
||||
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:
|
||||
return self.push_time + self.tot_offset
|
||||
return None
|
||||
|
||||
@property
|
||||
def push_time(self) -> timedelta:
|
||||
def push_time(self) -> datetime:
|
||||
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
|
||||
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
|
||||
|
||||
|
||||
|
||||
@ -3,7 +3,7 @@ from __future__ import annotations
|
||||
import random
|
||||
from collections.abc import Iterator
|
||||
from dataclasses import dataclass
|
||||
from datetime import timedelta
|
||||
from datetime import datetime, timedelta
|
||||
from typing import TYPE_CHECKING, Type
|
||||
|
||||
from game.utils import Distance, Speed, feet
|
||||
@ -71,20 +71,20 @@ class TarCapFlightPlan(PatrollingFlightPlan[TarCapLayout]):
|
||||
def default_tot_offset(self) -> timedelta:
|
||||
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:
|
||||
return self.patrol_end_time
|
||||
return super().depart_time_for_waypoint(waypoint)
|
||||
|
||||
@property
|
||||
def patrol_start_time(self) -> timedelta:
|
||||
def patrol_start_time(self) -> datetime:
|
||||
start = self.package.escort_start_time
|
||||
if start is not None:
|
||||
return start + self.tot_offset
|
||||
return self.tot
|
||||
|
||||
@property
|
||||
def patrol_end_time(self) -> timedelta:
|
||||
def patrol_end_time(self) -> datetime:
|
||||
end = self.package.escort_end_time
|
||||
if end is not None:
|
||||
return end
|
||||
|
||||
@ -35,7 +35,6 @@ class Uninitialized(FlightState):
|
||||
|
||||
@property
|
||||
def description(self) -> str:
|
||||
delay = self.flight.flight_plan.startup_time()
|
||||
if self.flight.start_type is StartType.COLD:
|
||||
action = "Starting up"
|
||||
elif self.flight.start_type is StartType.WARM:
|
||||
@ -46,4 +45,4 @@ class Uninitialized(FlightState):
|
||||
action = "In flight"
|
||||
else:
|
||||
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}"
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from datetime import timedelta
|
||||
from datetime import datetime
|
||||
from typing import Literal, TYPE_CHECKING
|
||||
|
||||
from dcs import Point
|
||||
@ -43,8 +43,8 @@ class FlightWaypoint:
|
||||
# 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
|
||||
# flight's offset in the UI.
|
||||
tot: timedelta | None = None
|
||||
departure_time: timedelta | None = None
|
||||
tot: datetime | None = None
|
||||
departure_time: datetime | None = None
|
||||
|
||||
@property
|
||||
def x(self) -> float:
|
||||
|
||||
@ -2,7 +2,7 @@ from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from collections import defaultdict
|
||||
from datetime import timedelta
|
||||
from datetime import datetime
|
||||
from typing import Dict, Optional, TYPE_CHECKING
|
||||
|
||||
from game.db import Database
|
||||
@ -42,8 +42,11 @@ class Package(RadioFrequencyContainer):
|
||||
self.auto_asap = auto_asap
|
||||
self.flights: list[Flight] = []
|
||||
|
||||
# Desired TOT as an offset from mission start.
|
||||
self.time_over_target: timedelta = timedelta()
|
||||
# Desired TOT as an offset from mission start. Obviously datetime.min is bogus,
|
||||
# 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
|
||||
|
||||
@property
|
||||
@ -71,7 +74,7 @@ class Package(RadioFrequencyContainer):
|
||||
# TODO: Should depend on the type of escort.
|
||||
# SEAD might be able to leave before CAP.
|
||||
@property
|
||||
def escort_start_time(self) -> Optional[timedelta]:
|
||||
def escort_start_time(self) -> datetime | None:
|
||||
times = []
|
||||
for flight in self.flights:
|
||||
waypoint = flight.flight_plan.request_escort_at()
|
||||
@ -90,7 +93,7 @@ class Package(RadioFrequencyContainer):
|
||||
return None
|
||||
|
||||
@property
|
||||
def escort_end_time(self) -> Optional[timedelta]:
|
||||
def escort_end_time(self) -> datetime | None:
|
||||
times = []
|
||||
for flight in self.flights:
|
||||
waypoint = flight.flight_plan.dismiss_escort_at()
|
||||
@ -112,7 +115,7 @@ class Package(RadioFrequencyContainer):
|
||||
return None
|
||||
|
||||
@property
|
||||
def mission_departure_time(self) -> Optional[timedelta]:
|
||||
def mission_departure_time(self) -> datetime | None:
|
||||
times = []
|
||||
for flight in self.flights:
|
||||
times.append(flight.flight_plan.mission_departure_time)
|
||||
@ -120,8 +123,8 @@ class Package(RadioFrequencyContainer):
|
||||
return max(times)
|
||||
return None
|
||||
|
||||
def set_tot_asap(self) -> None:
|
||||
self.time_over_target = TotEstimator(self).earliest_tot()
|
||||
def set_tot_asap(self, now: datetime) -> None:
|
||||
self.time_over_target = TotEstimator(self).earliest_tot(now)
|
||||
|
||||
def add_flight(self, flight: Flight) -> None:
|
||||
"""Adds a flight to the package."""
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import math
|
||||
from datetime import timedelta
|
||||
from datetime import datetime, timedelta
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
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:
|
||||
self.package = package
|
||||
|
||||
def earliest_tot(self) -> timedelta:
|
||||
def earliest_tot(self, now: datetime) -> datetime:
|
||||
if not self.package.flights:
|
||||
return timedelta(0)
|
||||
return now
|
||||
|
||||
earliest_tot = max(
|
||||
(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()))
|
||||
return max(self.earliest_tot_for_flight(f, now) for f in self.package.flights)
|
||||
|
||||
@staticmethod
|
||||
def earliest_tot_for_flight(flight: Flight) -> timedelta:
|
||||
"""Estimate the fastest time from mission start to the target position.
|
||||
def earliest_tot_for_flight(flight: Flight, now: datetime) -> datetime:
|
||||
"""Estimate the earliest time the flight can reach the target position.
|
||||
|
||||
For BARCAP flights, this is time to the racetrack start. This ensures that
|
||||
they are on station at the same time any other package members reach
|
||||
their ingress point.
|
||||
|
||||
For other mission types this is the time to the mission target.
|
||||
The interpretation of the TOT depends on the flight plan type. See the various
|
||||
FlightPlan implementations for details.
|
||||
|
||||
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:
|
||||
The earliest possible TOT for the given flight in seconds. Returns 0
|
||||
if an ingress point cannot be found.
|
||||
The earliest possible TOT for the given flight.
|
||||
"""
|
||||
# Clear the TOT, calculate the startup time. Negating the result gives
|
||||
# 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
|
||||
return now + flight.flight_plan.minimum_duration_from_start_to_tot()
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime
|
||||
from typing import Any, Optional, TYPE_CHECKING
|
||||
|
||||
from faker import Faker
|
||||
@ -182,10 +183,10 @@ class Coalition:
|
||||
with logged_duration("Procurement of airlift assets"):
|
||||
self.transfers.order_airlift_assets()
|
||||
with logged_duration("Transport planning"):
|
||||
self.transfers.plan_transports()
|
||||
self.transfers.plan_transports(self.game.conditions.start_time)
|
||||
|
||||
if not is_turn_0:
|
||||
self.plan_missions()
|
||||
self.plan_missions(self.game.conditions.start_time)
|
||||
self.plan_procurement()
|
||||
|
||||
def refund_outstanding_orders(self) -> None:
|
||||
@ -201,16 +202,16 @@ class Coalition:
|
||||
for squadron in self.air_wing.iter_squadrons():
|
||||
squadron.refund_orders()
|
||||
|
||||
def plan_missions(self) -> None:
|
||||
def plan_missions(self, now: datetime) -> None:
|
||||
color = "Blue" if self.player else "Red"
|
||||
with MultiEventTracer() as tracer:
|
||||
with tracer.trace(f"{color} mission planning"):
|
||||
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"):
|
||||
MissionScheduler(
|
||||
self, self.game.settings.desired_player_mission_duration
|
||||
).schedule_missions()
|
||||
).schedule_missions(now)
|
||||
|
||||
def plan_procurement(self) -> None:
|
||||
# The first turn needs to buy a *lot* of aircraft to fill CAPs, so it gets much
|
||||
|
||||
@ -3,8 +3,8 @@ from __future__ import annotations
|
||||
import logging
|
||||
import random
|
||||
from collections import defaultdict
|
||||
from datetime import timedelta
|
||||
from typing import Iterator, Dict, TYPE_CHECKING
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Iterator, TYPE_CHECKING
|
||||
|
||||
from game.ato.flighttype import FlightType
|
||||
from game.ato.traveltime import TotEstimator
|
||||
@ -19,7 +19,7 @@ class MissionScheduler:
|
||||
self.coalition = coalition
|
||||
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."""
|
||||
|
||||
def start_time_generator(
|
||||
@ -35,7 +35,7 @@ class MissionScheduler:
|
||||
FlightType.TARCAP,
|
||||
}
|
||||
|
||||
previous_cap_end_time: Dict[MissionTarget, timedelta] = defaultdict(timedelta)
|
||||
previous_cap_end_time: dict[MissionTarget, datetime] = defaultdict(now.replace)
|
||||
non_dca_packages = [
|
||||
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,
|
||||
)
|
||||
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:
|
||||
previous_end_time = previous_cap_end_time[package.target]
|
||||
if tot > previous_end_time:
|
||||
@ -65,7 +65,7 @@ class MissionScheduler:
|
||||
continue
|
||||
previous_cap_end_time[package.target] = departure_time
|
||||
elif package.auto_asap:
|
||||
package.set_tot_asap()
|
||||
package.set_tot_asap(now)
|
||||
else:
|
||||
# But other packages should be spread out a bit. Note that take
|
||||
# times are delayed, but all aircraft will become active at
|
||||
|
||||
@ -2,6 +2,7 @@ from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from collections import defaultdict
|
||||
from datetime import datetime
|
||||
from typing import Dict, Iterable, Optional, Set, TYPE_CHECKING
|
||||
|
||||
from game.ato.airtaaskingorder import AirTaskingOrder
|
||||
@ -135,6 +136,7 @@ class PackageFulfiller:
|
||||
self,
|
||||
mission: ProposedMission,
|
||||
purchase_multiplier: int,
|
||||
now: datetime,
|
||||
tracer: MultiEventTracer,
|
||||
) -> Optional[Package]:
|
||||
"""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:
|
||||
package.auto_asap = True
|
||||
package.set_tot_asap()
|
||||
package.set_tot_asap(now)
|
||||
|
||||
return package
|
||||
|
||||
@ -105,6 +105,7 @@ class PackagePlanningTask(TheaterCommanderTask, Generic[MissionTargetT]):
|
||||
self.package = fulfiller.plan_mission(
|
||||
ProposedMission(self.target, self.flights),
|
||||
self.purchase_multiplier,
|
||||
state.context.now,
|
||||
state.context.tracer,
|
||||
)
|
||||
return self.package is not None
|
||||
|
||||
@ -54,6 +54,7 @@ https://en.wikipedia.org/wiki/Hierarchical_task_network
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from game.ato.starttype import StartType
|
||||
@ -77,8 +78,8 @@ class TheaterCommander(Planner[TheaterState, TheaterCommanderTask]):
|
||||
self.game = game
|
||||
self.player = player
|
||||
|
||||
def plan_missions(self, tracer: MultiEventTracer) -> None:
|
||||
state = TheaterState.from_game(self.game, self.player, tracer)
|
||||
def plan_missions(self, now: datetime, tracer: MultiEventTracer) -> None:
|
||||
state = TheaterState.from_game(self.game, self.player, now, tracer)
|
||||
while True:
|
||||
result = self.plan(state)
|
||||
if result is None:
|
||||
|
||||
@ -5,6 +5,7 @@ import itertools
|
||||
import math
|
||||
from collections.abc import Iterator
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime
|
||||
from typing import Optional, TYPE_CHECKING, Union
|
||||
|
||||
from game.commander.battlepositions import BattlePositions
|
||||
@ -36,6 +37,7 @@ class PersistentContext:
|
||||
coalition: Coalition
|
||||
theater: ConflictTheater
|
||||
turn: int
|
||||
now: datetime
|
||||
settings: Settings
|
||||
tracer: MultiEventTracer
|
||||
|
||||
@ -139,14 +141,20 @@ class TheaterState(WorldState["TheaterState"]):
|
||||
|
||||
@classmethod
|
||||
def from_game(
|
||||
cls, game: Game, player: bool, tracer: MultiEventTracer
|
||||
cls, game: Game, player: bool, now: datetime, tracer: MultiEventTracer
|
||||
) -> TheaterState:
|
||||
coalition = game.coalition_for(player)
|
||||
finder = ObjectiveFinder(game, player)
|
||||
ordered_capturable_points = finder.prioritized_unisolated_points()
|
||||
|
||||
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
|
||||
|
||||
@ -2,6 +2,7 @@ from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from collections import defaultdict
|
||||
from datetime import datetime
|
||||
from typing import Optional, TYPE_CHECKING
|
||||
|
||||
from game.theater import ControlPoint
|
||||
@ -52,7 +53,7 @@ class GroundUnitOrders:
|
||||
pending_units = 0
|
||||
return pending_units
|
||||
|
||||
def process(self, game: Game) -> None:
|
||||
def process(self, game: Game, now: datetime) -> None:
|
||||
coalition = game.coalition_for(self.destination.captured)
|
||||
ground_unit_source = self.find_ground_unit_source(game)
|
||||
if ground_unit_source is None:
|
||||
@ -95,15 +96,20 @@ class GroundUnitOrders:
|
||||
"still tried to transfer units to there"
|
||||
)
|
||||
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(
|
||||
self,
|
||||
coalition: Coalition,
|
||||
source: ControlPoint,
|
||||
units: dict[GroundUnitType, int],
|
||||
now: datetime,
|
||||
) -> 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]:
|
||||
# This is running *after* the turn counter has been incremented, so this is the
|
||||
|
||||
@ -97,7 +97,6 @@ class FlightGroupConfigurator:
|
||||
self.flight,
|
||||
self.group,
|
||||
self.mission,
|
||||
self.game.conditions.start_time,
|
||||
self.time,
|
||||
self.game.settings,
|
||||
self.mission_data,
|
||||
|
||||
@ -31,7 +31,7 @@ class HoldPointBuilder(PydcsWaypointBuilder):
|
||||
return
|
||||
push_time = self.flight.flight_plan.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)
|
||||
# What follows is some code to cope with the broken 'stop after time' condition
|
||||
create_stop_orbit_trigger(loiter, self.package, self.mission, elapsed)
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import timedelta
|
||||
from datetime import datetime
|
||||
from typing import Any, Iterable, Union
|
||||
|
||||
from dcs import Mission
|
||||
@ -28,7 +28,7 @@ class PydcsWaypointBuilder:
|
||||
group: FlyingGroup[Any],
|
||||
flight: Flight,
|
||||
mission: Mission,
|
||||
elapsed_mission_time: timedelta,
|
||||
now: datetime,
|
||||
mission_data: MissionData,
|
||||
) -> None:
|
||||
self.waypoint = waypoint
|
||||
@ -36,7 +36,7 @@ class PydcsWaypointBuilder:
|
||||
self.package = flight.package
|
||||
self.flight = flight
|
||||
self.mission = mission
|
||||
self.elapsed_mission_time = elapsed_mission_time
|
||||
self.now = now
|
||||
self.mission_data = mission_data
|
||||
|
||||
def dcs_name_for_waypoint(self) -> str:
|
||||
@ -83,10 +83,10 @@ class PydcsWaypointBuilder:
|
||||
def add_tasks(self, waypoint: MovingPoint) -> None:
|
||||
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
|
||||
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.speed_locked = False
|
||||
|
||||
|
||||
@ -57,7 +57,7 @@ class RaceTrackBuilder(PydcsWaypointBuilder):
|
||||
|
||||
racetrack = ControlledTask(orbit)
|
||||
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())
|
||||
racetrack.stop_after_time(elapsed)
|
||||
# What follows is some code to cope with the broken 'stop after time' condition
|
||||
|
||||
@ -50,7 +50,6 @@ class WaypointGenerator:
|
||||
flight: Flight,
|
||||
group: FlyingGroup[Any],
|
||||
mission: Mission,
|
||||
turn_start_time: datetime,
|
||||
time: datetime,
|
||||
settings: Settings,
|
||||
mission_data: MissionData,
|
||||
@ -58,7 +57,6 @@ class WaypointGenerator:
|
||||
self.flight = flight
|
||||
self.group = group
|
||||
self.mission = mission
|
||||
self.elapsed_mission_time = time - turn_start_time
|
||||
self.time = time
|
||||
self.settings = settings
|
||||
self.mission_data = mission_data
|
||||
@ -152,7 +150,7 @@ class WaypointGenerator:
|
||||
self.group,
|
||||
self.flight,
|
||||
self.mission,
|
||||
self.elapsed_mission_time,
|
||||
self.time,
|
||||
self.mission_data,
|
||||
)
|
||||
|
||||
@ -183,12 +181,29 @@ class WaypointGenerator:
|
||||
a.min_fuel = min_fuel
|
||||
|
||||
def set_takeoff_time(self, waypoint: FlightWaypoint) -> timedelta:
|
||||
force_delay = False
|
||||
if isinstance(self.flight.state, WaitingForStart):
|
||||
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:
|
||||
delay = timedelta()
|
||||
|
||||
if self.should_delay_flight():
|
||||
if force_delay or self.should_delay_flight():
|
||||
if self.should_activate_late():
|
||||
# Late activation causes the aircraft to not be spawned
|
||||
# until triggered.
|
||||
|
||||
@ -5,7 +5,6 @@ from __future__ import annotations
|
||||
|
||||
import os
|
||||
from dataclasses import dataclass
|
||||
from datetime import timedelta
|
||||
from typing import Dict, List, TYPE_CHECKING
|
||||
|
||||
from dcs.mission import Mission
|
||||
@ -130,11 +129,9 @@ class MissionInfoGenerator:
|
||||
|
||||
def format_waypoint_time(waypoint: FlightWaypoint, depart_prefix: str) -> str:
|
||||
if waypoint.tot is not None:
|
||||
time = timedelta(seconds=int(waypoint.tot.total_seconds()))
|
||||
return f"T+{time} "
|
||||
return f"{waypoint.tot.time()} "
|
||||
elif waypoint.departure_time is not None:
|
||||
time = timedelta(seconds=int(waypoint.departure_time.total_seconds()))
|
||||
return f"{depart_prefix} T+{time} "
|
||||
return f"{depart_prefix} {waypoint.departure_time.time()} "
|
||||
return ""
|
||||
|
||||
|
||||
|
||||
@ -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:
|
||||
return ""
|
||||
local_time = self.start_time + time
|
||||
return f"{local_time.strftime('%H:%M:%S')}{'Z' if local_time.tzinfo is not None else ''}"
|
||||
return f"{time.strftime('%H:%M:%S')}{'Z' if time.tzinfo is not None else ''}"
|
||||
|
||||
def _format_alt(self, alt: Distance) -> str:
|
||||
return f"{self.units.distance_short(alt):.0f}"
|
||||
@ -643,11 +643,11 @@ class SupportPage(KneeboardPage):
|
||||
)
|
||||
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:
|
||||
return ""
|
||||
local_time = self.start_time + time
|
||||
return f"{local_time.strftime('%H:%M:%S')}{'Z' if local_time.tzinfo is not None else ''}"
|
||||
return f"{time.strftime('%H:%M:%S')}{'Z' if time.tzinfo is not None else ''}"
|
||||
|
||||
@staticmethod
|
||||
def _format_duration(time: Optional[datetime.timedelta]) -> str:
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from datetime import timedelta
|
||||
from datetime import datetime
|
||||
from typing import Optional, TYPE_CHECKING
|
||||
|
||||
from game.dcs.aircrafttype import AircraftType
|
||||
@ -33,8 +33,8 @@ class AwacsInfo(GroupInfo):
|
||||
"""AWACS information for the kneeboard."""
|
||||
|
||||
depature_location: Optional[str]
|
||||
start_time: timedelta
|
||||
end_time: timedelta
|
||||
start_time: datetime | None
|
||||
end_time: datetime | None
|
||||
|
||||
|
||||
@dataclass
|
||||
@ -43,8 +43,8 @@ class TankerInfo(GroupInfo):
|
||||
|
||||
variant: str
|
||||
tacan: Optional[TacanChannel]
|
||||
start_time: timedelta
|
||||
end_time: timedelta
|
||||
start_time: datetime | None
|
||||
end_time: datetime | None
|
||||
|
||||
|
||||
@dataclass
|
||||
|
||||
@ -1,7 +1,5 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import timedelta
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
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)
|
||||
if time is None:
|
||||
return ""
|
||||
return f"{prefix} T+{timedelta(seconds=int(time.total_seconds()))}"
|
||||
return f"{prefix} {time}"
|
||||
|
||||
|
||||
class FlightWaypointJs(BaseModel):
|
||||
|
||||
@ -74,11 +74,11 @@ class AircraftSimulation:
|
||||
now = self.game.conditions.start_time
|
||||
for flight in self.iter_flights():
|
||||
start_time = flight.flight_plan.startup_time()
|
||||
if start_time <= timedelta():
|
||||
if start_time <= now:
|
||||
self.set_active_flight_state(flight, now)
|
||||
else:
|
||||
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:
|
||||
|
||||
@ -4,7 +4,9 @@ import logging
|
||||
import random
|
||||
from collections.abc import Iterable
|
||||
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 dcs.country import Country
|
||||
@ -380,9 +382,8 @@ class Squadron:
|
||||
def arrival(self) -> ControlPoint:
|
||||
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
|
||||
|
||||
if destination == self.location:
|
||||
logging.warning(
|
||||
f"Attempted to plan relocation of {self} to current location "
|
||||
@ -402,7 +403,7 @@ class Squadron:
|
||||
if not destination.can_operate(self.aircraft):
|
||||
raise RuntimeError(f"{self} cannot operate at {destination}.")
|
||||
self.destination = destination
|
||||
self.replan_ferry_flights()
|
||||
self.replan_ferry_flights(now)
|
||||
|
||||
def cancel_relocation(self) -> None:
|
||||
from game.theater import ParkingType
|
||||
@ -420,9 +421,9 @@ class Squadron:
|
||||
self.destination = None
|
||||
self.cancel_ferry_flights()
|
||||
|
||||
def replan_ferry_flights(self) -> None:
|
||||
def replan_ferry_flights(self, now: datetime) -> None:
|
||||
self.cancel_ferry_flights()
|
||||
self.plan_ferry_flights()
|
||||
self.plan_ferry_flights(now)
|
||||
|
||||
def cancel_ferry_flights(self) -> None:
|
||||
for package in self.coalition.ato.packages:
|
||||
@ -433,7 +434,7 @@ class Squadron:
|
||||
if not package.flights:
|
||||
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:
|
||||
raise RuntimeError(
|
||||
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)
|
||||
self.plan_ferry_flight(package, size)
|
||||
remaining -= size
|
||||
package.set_tot_asap()
|
||||
package.set_tot_asap(now)
|
||||
self.coalition.ato.add_package(package)
|
||||
|
||||
def plan_ferry_flight(self, package: Package, size: int) -> None:
|
||||
|
||||
@ -1011,7 +1011,11 @@ class ControlPoint(MissionTarget, SidcDescribable, ABC):
|
||||
self.runway_status.begin_repair()
|
||||
|
||||
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
|
||||
if runway_status is not None:
|
||||
|
||||
@ -35,6 +35,7 @@ import logging
|
||||
import math
|
||||
from collections import defaultdict
|
||||
from dataclasses import dataclass, field
|
||||
from datetime import datetime
|
||||
from functools import singledispatchmethod
|
||||
from typing import Generic, Iterator, List, Optional, Sequence, TYPE_CHECKING, TypeVar
|
||||
|
||||
@ -299,7 +300,7 @@ class AirliftPlanner:
|
||||
|
||||
return True
|
||||
|
||||
def create_package_for_airlift(self) -> None:
|
||||
def create_package_for_airlift(self, now: datetime) -> None:
|
||||
distance_cache = ObjectiveDistanceCache.get_closest_airfields(
|
||||
self.transfer.position
|
||||
)
|
||||
@ -318,7 +319,7 @@ class AirliftPlanner:
|
||||
):
|
||||
self.create_airlift_flight(squadron)
|
||||
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)
|
||||
|
||||
def create_airlift_flight(self, squadron: Squadron) -> int:
|
||||
@ -590,7 +591,7 @@ class PendingTransfers:
|
||||
def network_for(self, control_point: ControlPoint) -> TransitNetwork:
|
||||
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)
|
||||
path = network.shortest_path_between(transfer.position, transfer.destination)
|
||||
next_stop = path[0]
|
||||
@ -605,12 +606,12 @@ class PendingTransfers:
|
||||
== TransitConnection.Shipping
|
||||
):
|
||||
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)
|
||||
self.pending_transfers.append(transfer)
|
||||
self.arrange_transport(transfer)
|
||||
self.arrange_transport(transfer, now)
|
||||
self._send_supply_route_event_stream_update()
|
||||
|
||||
def split_transfer(self, transfer: TransferOrder, size: int) -> TransferOrder:
|
||||
@ -692,7 +693,7 @@ class PendingTransfers:
|
||||
self.cargo_ships.disband_all()
|
||||
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
|
||||
transport assigned already. This calculates the shortest path between current
|
||||
@ -702,7 +703,7 @@ class PendingTransfers:
|
||||
self.disband_uncompletable_transfers()
|
||||
for transfer in self.pending_transfers:
|
||||
if transfer.transport is None:
|
||||
self.arrange_transport(transfer)
|
||||
self.arrange_transport(transfer, now)
|
||||
|
||||
def disband_uncompletable_transfers(self) -> None:
|
||||
"""
|
||||
|
||||
@ -142,11 +142,9 @@ class PackageModel(QAbstractListModel):
|
||||
@staticmethod
|
||||
def text_for_flight(flight: Flight) -> str:
|
||||
"""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
|
||||
return f"{flight} from {origin} in {delay}"
|
||||
startup = flight.flight_plan.startup_time()
|
||||
return f"{flight} from {origin} at {startup}"
|
||||
|
||||
@staticmethod
|
||||
def icon_for_flight(flight: Flight) -> Optional[QIcon]:
|
||||
@ -191,7 +189,7 @@ class PackageModel(QAbstractListModel):
|
||||
"""Returns the flight located at the given index."""
|
||||
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.update_tot()
|
||||
|
||||
@ -201,7 +199,9 @@ class PackageModel(QAbstractListModel):
|
||||
|
||||
def update_tot(self) -> None:
|
||||
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()
|
||||
# For some reason this is needed to make the UI update quickly.
|
||||
self.layoutChanged.emit()
|
||||
@ -395,11 +395,11 @@ class TransferModel(QAbstractListModel):
|
||||
"""Returns the icon that should be displayed for the transfer."""
|
||||
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."""
|
||||
self.beginInsertRows(QModelIndex(), self.rowCount(), self.rowCount())
|
||||
# TODO: Needs to regenerate base inventory tab.
|
||||
self.transfers.new_transfer(transfer)
|
||||
self.transfers.new_transfer(transfer, now)
|
||||
self.endInsertRows()
|
||||
|
||||
def cancel_transfer_at_index(self, index: QModelIndex) -> None:
|
||||
|
||||
@ -34,11 +34,18 @@ class SimController(QObject):
|
||||
return self.game_loop.completed
|
||||
|
||||
@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:
|
||||
return None
|
||||
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
|
||||
def elapsed_time(self) -> timedelta:
|
||||
if self.game_loop is None:
|
||||
|
||||
@ -58,7 +58,7 @@ class QTimeTurnWidget(QGroupBox):
|
||||
sim_controller.sim_update.connect(self.on_sim_update)
|
||||
|
||||
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:
|
||||
self.date_display.setText("")
|
||||
self.time_display.setText("")
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
from datetime import datetime
|
||||
from typing import List, Optional
|
||||
|
||||
from PySide2.QtWidgets import (
|
||||
@ -166,7 +167,7 @@ class QTopPanel(QFrame):
|
||||
GameUpdateSignal.get_instance().updateGame(self.game)
|
||||
self.proceedButton.setEnabled(True)
|
||||
|
||||
def negative_start_packages(self) -> List[Package]:
|
||||
def negative_start_packages(self, now: datetime) -> List[Package]:
|
||||
packages = []
|
||||
for package in self.game_model.ato_model.ato.packages:
|
||||
if not package.flights:
|
||||
@ -174,7 +175,7 @@ class QTopPanel(QFrame):
|
||||
for flight in package.flights:
|
||||
if flight.state.is_waiting_for_start:
|
||||
startup = flight.flight_plan.startup_time()
|
||||
if startup < 0:
|
||||
if startup < now:
|
||||
packages.append(package)
|
||||
break
|
||||
return packages
|
||||
@ -290,7 +291,9 @@ class QTopPanel(QFrame):
|
||||
if self.check_no_missing_pilots():
|
||||
return
|
||||
|
||||
negative_starts = self.negative_start_packages()
|
||||
negative_starts = self.negative_start_packages(
|
||||
self.sim_controller.current_time_in_sim
|
||||
)
|
||||
if negative_starts:
|
||||
if not self.confirm_negative_start_time(negative_starts):
|
||||
return
|
||||
|
||||
@ -1,6 +1,5 @@
|
||||
"""Widgets for displaying air tasking orders."""
|
||||
import logging
|
||||
from datetime import timedelta
|
||||
from typing import Optional
|
||||
|
||||
from PySide2.QtCore import (
|
||||
@ -285,16 +284,7 @@ class PackageDelegate(TwoColumnRowDelegate):
|
||||
clients = self.num_clients(index)
|
||||
return f"Player Slots: {clients}" if clients else ""
|
||||
elif (row, column) == (1, 0):
|
||||
tot_delay = (
|
||||
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}"
|
||||
return f"TOT at {package.time_over_target:%H:%M:%S}"
|
||||
elif (row, column) == (1, 1):
|
||||
unassigned_pilots = self.missing_pilots(index)
|
||||
return f"Missing pilots: {unassigned_pilots}" if unassigned_pilots else ""
|
||||
|
||||
@ -1,27 +1,28 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Optional, Iterator
|
||||
from typing import Iterator, Optional
|
||||
|
||||
from PySide2.QtCore import QItemSelectionModel, QModelIndex, QSize
|
||||
from PySide2.QtWidgets import (
|
||||
QAbstractItemView,
|
||||
QCheckBox,
|
||||
QDialog,
|
||||
QHBoxLayout,
|
||||
QListView,
|
||||
QVBoxLayout,
|
||||
QTabWidget,
|
||||
QTableWidget,
|
||||
QTableWidgetItem,
|
||||
QVBoxLayout,
|
||||
QWidget,
|
||||
QHBoxLayout,
|
||||
)
|
||||
|
||||
from game.ato.flight import Flight
|
||||
from game.squadrons import Squadron
|
||||
from game.theater import ConflictTheater
|
||||
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
|
||||
|
||||
|
||||
@ -63,11 +64,13 @@ class SquadronList(QListView):
|
||||
ato_model: AtoModel,
|
||||
air_wing_model: AirWingModel,
|
||||
theater: ConflictTheater,
|
||||
sim_controller: SimController,
|
||||
) -> None:
|
||||
super().__init__()
|
||||
self.ato_model = ato_model
|
||||
self.air_wing_model = air_wing_model
|
||||
self.theater = theater
|
||||
self.sim_controller = sim_controller
|
||||
self.dialog: Optional[SquadronDialog] = None
|
||||
|
||||
self.setIconSize(QSize(91, 24))
|
||||
@ -88,6 +91,7 @@ class SquadronList(QListView):
|
||||
self.ato_model,
|
||||
SquadronModel(self.air_wing_model.squadron_at_index(index)),
|
||||
self.theater,
|
||||
self.sim_controller,
|
||||
self,
|
||||
)
|
||||
self.dialog.show()
|
||||
@ -234,6 +238,7 @@ class AirWingTabs(QTabWidget):
|
||||
game_model.ato_model,
|
||||
game_model.blue_air_wing_model,
|
||||
game_model.game.theater,
|
||||
game_model.sim_controller,
|
||||
),
|
||||
"Squadrons",
|
||||
)
|
||||
|
||||
@ -30,6 +30,7 @@ from game.theater import ConflictTheater, ControlPoint, ParkingType
|
||||
from qt_ui.delegates import TwoColumnRowDelegate
|
||||
from qt_ui.errorreporter import report_errors
|
||||
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.primarytaskselector import PrimaryTaskSelector
|
||||
|
||||
@ -230,11 +231,13 @@ class SquadronDialog(QDialog):
|
||||
ato_model: AtoModel,
|
||||
squadron_model: SquadronModel,
|
||||
theater: ConflictTheater,
|
||||
sim_controller: SimController,
|
||||
parent,
|
||||
) -> None:
|
||||
super().__init__(parent)
|
||||
self.ato_model = ato_model
|
||||
self.squadron_model = squadron_model
|
||||
self.sim_controller = sim_controller
|
||||
|
||||
self.setMinimumSize(1000, 440)
|
||||
self.setWindowTitle(str(squadron_model.squadron))
|
||||
@ -327,7 +330,9 @@ class SquadronDialog(QDialog):
|
||||
elif self.ato_model.game.settings.enable_transfer_cheat:
|
||||
self._instant_relocate(destination)
|
||||
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)
|
||||
|
||||
def check_disabled_button_states(
|
||||
|
||||
@ -320,7 +320,9 @@ class NewUnitTransferDialog(QDialog):
|
||||
units=transfers,
|
||||
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()
|
||||
|
||||
def on_transfer_quantity_changed(self) -> None:
|
||||
|
||||
@ -19,4 +19,4 @@ class QFlightItem(QStandardItem):
|
||||
)
|
||||
self.setIcon(icon)
|
||||
self.setEditable(False)
|
||||
self.setText(f"{flight} in {flight.flight_plan.startup_time()}")
|
||||
self.setText(f"{flight} at {flight.flight_plan.startup_time()}")
|
||||
|
||||
@ -1,6 +1,5 @@
|
||||
"""Dialogs for creating and editing ATO packages."""
|
||||
import logging
|
||||
from datetime import timedelta
|
||||
from typing import Optional
|
||||
|
||||
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.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.setToolTip("Package TOT relative to mission TOT")
|
||||
self.tot_spinner.setEnabled(not self.package_model.package.auto_asap)
|
||||
@ -159,11 +158,8 @@ class QPackageDialog(QDialog):
|
||||
return self.game_model.game
|
||||
|
||||
def tot_qtime(self) -> QTime:
|
||||
delay = int(self.package_model.package.time_over_target.total_seconds())
|
||||
hours = delay // 3600
|
||||
minutes = delay // 60 % 60
|
||||
seconds = delay % 60
|
||||
return QTime(hours, minutes, seconds)
|
||||
tot = self.package_model.package.time_over_target
|
||||
return QTime(tot.hour, tot.minute, tot.second)
|
||||
|
||||
def on_cancel(self) -> None:
|
||||
pass
|
||||
@ -177,9 +173,13 @@ class QPackageDialog(QDialog):
|
||||
self.save_tot()
|
||||
|
||||
def save_tot(self) -> None:
|
||||
# TODO: This is going to break horribly around midnight.
|
||||
time = self.tot_spinner.time()
|
||||
seconds = time.hour() * 3600 + time.minute() * 60 + time.second()
|
||||
self.package_model.set_tot(timedelta(seconds=seconds))
|
||||
self.package_model.set_tot(
|
||||
self.package_model.package.time_over_target.replace(
|
||||
hour=time.hour(), minute=time.minute(), second=time.second()
|
||||
)
|
||||
)
|
||||
|
||||
def set_asap(self, checked: bool) -> None:
|
||||
self.package_model.set_asap(checked)
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
from datetime import timedelta
|
||||
from datetime import datetime
|
||||
|
||||
from PySide2.QtCore import QItemSelectionModel, QSize
|
||||
from PySide2.QtGui import QStandardItemModel
|
||||
@ -50,5 +50,5 @@ class QPlannedFlightsView(QListView):
|
||||
self.setup_content()
|
||||
|
||||
@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()
|
||||
|
||||
@ -102,7 +102,9 @@ class FlightPlanPropertiesGroup(QGroupBox):
|
||||
# handler may be called for a flight whose package has been canceled, which
|
||||
# is an invalid state for calling anything in TotEstimator.
|
||||
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()
|
||||
|
||||
def set_divert(self, index: int) -> None:
|
||||
|
||||
@ -1,5 +1,3 @@
|
||||
from datetime import timedelta
|
||||
|
||||
from PySide2.QtCore import QItemSelectionModel, QPoint, QModelIndex
|
||||
from PySide2.QtGui import QStandardItem, QStandardItemModel
|
||||
from PySide2.QtWidgets import (
|
||||
@ -120,14 +118,8 @@ class QFlightWaypointList(QTableView):
|
||||
time = flight.flight_plan.depart_time_for_waypoint(waypoint)
|
||||
if time is None:
|
||||
return ""
|
||||
time = timedelta(seconds=int(time.total_seconds()))
|
||||
return f"{prefix}T+{time}"
|
||||
return f"{prefix}{time:%H:%M:%S}"
|
||||
|
||||
@staticmethod
|
||||
def takeoff_text(flight: Flight) -> str:
|
||||
takeoff_time = flight.flight_plan.takeoff_time()
|
||||
# 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}"
|
||||
return f"{flight.flight_plan.takeoff_time():%H:%M:%S}"
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user