mirror of
https://github.com/dcs-liberation/dcs_liberation.git
synced 2025-11-10 14:22:26 +00:00
Convert TOTs to datetime.
https://github.com/dcs-liberation/dcs_liberation/issues/1680
This commit is contained in:
parent
ac6cc39616
commit
fd2ba6b2b2
@ -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.ato.flightplans.standard import StandardFlightPlan, StandardLayout
|
||||
@ -55,12 +55,12 @@ class AirAssaultFlightPlan(StandardFlightPlan[AirAssaultLayout], UiZoneDisplay):
|
||||
def tot_waypoint(self) -> FlightWaypoint:
|
||||
return self.layout.drop_off
|
||||
|
||||
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.tot
|
||||
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
|
||||
@ -68,11 +68,11 @@ class AirAssaultFlightPlan(StandardFlightPlan[AirAssaultLayout], UiZoneDisplay):
|
||||
return meters(2500)
|
||||
|
||||
@property
|
||||
def mission_begin_on_station_time(self) -> timedelta | None:
|
||||
def mission_begin_on_station_time(self) -> datetime | None:
|
||||
return None
|
||||
|
||||
@property
|
||||
def mission_departure_time(self) -> timedelta:
|
||||
def mission_departure_time(self) -> datetime:
|
||||
return self.package.time_over_target
|
||||
|
||||
def ui_zone(self) -> UiZone:
|
||||
|
||||
@ -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.theater.missiontarget import MissionTarget
|
||||
@ -67,20 +67,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_begin_on_station_time(self) -> timedelta | None:
|
||||
def mission_begin_on_station_time(self) -> datetime | None:
|
||||
return None
|
||||
|
||||
@property
|
||||
def mission_departure_time(self) -> timedelta:
|
||||
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
|
||||
@ -42,20 +42,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
|
||||
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_begin_on_station_time(self) -> timedelta | None:
|
||||
def mission_begin_on_station_time(self) -> datetime | None:
|
||||
return None
|
||||
|
||||
@property
|
||||
def mission_departure_time(self) -> timedelta:
|
||||
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
|
||||
@ -37,20 +37,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_begin_on_station_time(self) -> timedelta | None:
|
||||
def mission_begin_on_station_time(self) -> datetime | None:
|
||||
return None
|
||||
|
||||
@property
|
||||
def mission_departure_time(self) -> timedelta:
|
||||
def mission_departure_time(self) -> datetime:
|
||||
return self.package.time_over_target
|
||||
|
||||
|
||||
|
||||
@ -11,7 +11,7 @@ import math
|
||||
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
|
||||
|
||||
@ -149,7 +149,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
|
||||
@ -215,7 +215,13 @@ class FlightPlan(ABC, Generic[LayoutT]):
|
||||
|
||||
for previous_waypoint, waypoint in self.edges(until=destination):
|
||||
total += self.travel_time_between_waypoints(previous_waypoint, waypoint)
|
||||
return total
|
||||
|
||||
# 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
|
||||
# mission planning perspective, so there's little value to keeping them in the
|
||||
# model.
|
||||
return timedelta(seconds=math.floor(total.total_seconds()))
|
||||
|
||||
def travel_time_between_waypoints(
|
||||
self, a: FlightWaypoint, b: FlightWaypoint
|
||||
@ -224,10 +230,10 @@ class FlightPlan(ABC, Generic[LayoutT]):
|
||||
a.position, b.position, self.speed_between_waypoints(a, b)
|
||||
)
|
||||
|
||||
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:
|
||||
@ -250,34 +256,20 @@ 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
|
||||
# zero.
|
||||
if math.isclose(start_time.total_seconds(), 0):
|
||||
start_time = timedelta()
|
||||
|
||||
# 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 down so *barely* above zero start times are just zero.
|
||||
start_time = timedelta(seconds=math.floor(start_time.total_seconds()))
|
||||
|
||||
# Feature request #1309: Carrier planes should start at +1s
|
||||
# This is a workaround to a DCS problem: some AI planes spawn on
|
||||
# the 'sixpack' when start_time is zero and cause a deadlock.
|
||||
# Workaround: force the start_time to 1 second for these planes.
|
||||
if self.flight.from_cp.is_fleet and start_time.total_seconds() == 0:
|
||||
start_time = timedelta(seconds=1)
|
||||
|
||||
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:
|
||||
@ -298,7 +290,7 @@ class FlightPlan(ABC, Generic[LayoutT]):
|
||||
|
||||
@property
|
||||
@abstractmethod
|
||||
def mission_begin_on_station_time(self) -> timedelta | None:
|
||||
def mission_begin_on_station_time(self) -> datetime | None:
|
||||
"""The time that the mission is first on-station.
|
||||
|
||||
Not all mission types will have a time when they can be considered on-station.
|
||||
@ -307,7 +299,7 @@ class FlightPlan(ABC, Generic[LayoutT]):
|
||||
"""
|
||||
|
||||
@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
|
||||
|
||||
@ -73,15 +73,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
|
||||
elif waypoint == self.layout.split:
|
||||
@ -89,7 +89,7 @@ class FormationFlightPlan(LoiterFlightPlan, ABC):
|
||||
return None
|
||||
|
||||
@property
|
||||
def push_time(self) -> timedelta:
|
||||
def push_time(self) -> datetime:
|
||||
return self.join_time - TravelTime.between_points(
|
||||
self.layout.hold.position,
|
||||
self.layout.join.position,
|
||||
@ -97,11 +97,11 @@ class FormationFlightPlan(LoiterFlightPlan, ABC):
|
||||
)
|
||||
|
||||
@property
|
||||
def mission_begin_on_station_time(self) -> timedelta | None:
|
||||
def mission_begin_on_station_time(self) -> datetime | None:
|
||||
return None
|
||||
|
||||
@property
|
||||
def mission_departure_time(self) -> timedelta:
|
||||
def mission_departure_time(self) -> datetime:
|
||||
return self.split_time
|
||||
|
||||
@self_type_guard
|
||||
|
||||
@ -3,7 +3,7 @@ from __future__ import annotations
|
||||
from abc import ABC
|
||||
from collections.abc import Iterator
|
||||
from dataclasses import dataclass
|
||||
from datetime import timedelta
|
||||
from datetime import datetime, timedelta
|
||||
from typing import TYPE_CHECKING, TypeVar
|
||||
|
||||
from dcs import Point
|
||||
@ -91,14 +91,14 @@ class FormationAttackFlightPlan(FormationFlightPlan, ABC):
|
||||
return total
|
||||
|
||||
@property
|
||||
def join_time(self) -> timedelta:
|
||||
def join_time(self) -> datetime:
|
||||
travel_time = self.travel_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.travel_time_between_waypoints(
|
||||
self.layout.ingress, self.target_area_waypoint
|
||||
)
|
||||
@ -115,14 +115,14 @@ class FormationAttackFlightPlan(FormationFlightPlan, ABC):
|
||||
)
|
||||
|
||||
@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.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 in self.layout.targets:
|
||||
|
||||
@ -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 typing import Any, TYPE_CHECKING, TypeGuard
|
||||
|
||||
from game.typeguard import self_type_guard
|
||||
@ -25,10 +25,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
|
||||
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
|
||||
@ -61,22 +61,22 @@ class PatrollingFlightPlan(StandardFlightPlan[LayoutT], UiZoneDisplay, ABC):
|
||||
"""
|
||||
|
||||
@property
|
||||
def patrol_start_time(self) -> timedelta:
|
||||
def patrol_start_time(self) -> datetime:
|
||||
return self.package.time_over_target
|
||||
|
||||
@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
|
||||
@ -90,11 +90,11 @@ class PatrollingFlightPlan(StandardFlightPlan[LayoutT], UiZoneDisplay, ABC):
|
||||
return self.layout.patrol_start
|
||||
|
||||
@property
|
||||
def mission_begin_on_station_time(self) -> timedelta:
|
||||
def mission_begin_on_station_time(self) -> datetime:
|
||||
return self.patrol_start_time
|
||||
|
||||
@property
|
||||
def mission_departure_time(self) -> timedelta:
|
||||
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
|
||||
@ -43,19 +43,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_begin_on_station_time(self) -> timedelta | None:
|
||||
def mission_begin_on_station_time(self) -> datetime | None:
|
||||
return None
|
||||
|
||||
@property
|
||||
def mission_departure_time(self) -> timedelta:
|
||||
return timedelta()
|
||||
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
|
||||
|
||||
from game.ato.flightplans.ibuilder import IBuilder
|
||||
@ -37,19 +37,27 @@ class RecoveryTankerFlightPlan(StandardFlightPlan[RecoveryTankerLayout]):
|
||||
return self.layout.recovery_ship
|
||||
|
||||
@property
|
||||
def mission_begin_on_station_time(self) -> timedelta:
|
||||
def mission_begin_on_station_time(self) -> datetime:
|
||||
return self.package.time_over_target
|
||||
|
||||
@property
|
||||
def mission_departure_time(self) -> timedelta:
|
||||
def mission_departure_time(self) -> datetime:
|
||||
return self.patrol_end_time
|
||||
|
||||
@property
|
||||
def patrol_start_time(self) -> datetime:
|
||||
return self.package.time_over_target
|
||||
|
||||
@property
|
||||
def patrol_end_time(self) -> datetime:
|
||||
return self.tot + timedelta(hours=2)
|
||||
|
||||
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.tot
|
||||
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.tot_waypoint:
|
||||
return self.mission_departure_time
|
||||
return None
|
||||
|
||||
@ -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
|
||||
@ -59,30 +59,30 @@ class SweepFlightPlan(LoiterFlightPlan):
|
||||
return -self.lead_time
|
||||
|
||||
@property
|
||||
def sweep_start_time(self) -> timedelta:
|
||||
def sweep_start_time(self) -> datetime:
|
||||
travel_time = self.travel_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
|
||||
if waypoint == self.layout.sweep_end:
|
||||
return self.sweep_end_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.hold:
|
||||
return self.push_time
|
||||
return None
|
||||
|
||||
@property
|
||||
def push_time(self) -> timedelta:
|
||||
def push_time(self) -> datetime:
|
||||
return self.sweep_end_time - TravelTime.between_points(
|
||||
self.layout.hold.position,
|
||||
self.layout.sweep_end.position,
|
||||
@ -90,10 +90,10 @@ class SweepFlightPlan(LoiterFlightPlan):
|
||||
)
|
||||
|
||||
@property
|
||||
def mission_begin_on_station_time(self) -> timedelta | None:
|
||||
def mission_begin_on_station_time(self) -> datetime | None:
|
||||
return None
|
||||
|
||||
def mission_departure_time(self) -> timedelta:
|
||||
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
|
||||
@ -68,20 +68,20 @@ class TarCapFlightPlan(PatrollingFlightPlan[TarCapLayout]):
|
||||
def tot_offset(self) -> timedelta:
|
||||
return -self.lead_time
|
||||
|
||||
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
|
||||
@ -33,8 +33,11 @@ class Package:
|
||||
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
|
||||
@ -62,7 +65,7 @@ class Package:
|
||||
# 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()
|
||||
@ -81,7 +84,7 @@ class Package:
|
||||
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()
|
||||
@ -103,7 +106,7 @@ class Package:
|
||||
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)
|
||||
@ -111,8 +114,8 @@ class Package:
|
||||
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 dcs.mapping import Point
|
||||
@ -56,44 +55,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
|
||||
@ -181,9 +182,9 @@ 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)
|
||||
|
||||
self.plan_missions()
|
||||
self.plan_missions(self.game.conditions.start_time)
|
||||
self.plan_procurement()
|
||||
|
||||
def refund_outstanding_orders(self) -> None:
|
||||
@ -199,16 +200,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,12 +3,12 @@ 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.theater import MissionTarget
|
||||
from game.ato.flighttype import FlightType
|
||||
from game.ato.traveltime import TotEstimator
|
||||
from game.theater import MissionTarget
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from game.coalition import Coalition
|
||||
@ -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
|
||||
@ -132,6 +133,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."""
|
||||
@ -221,6 +223,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
|
||||
|
||||
@ -104,6 +104,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
|
||||
|
||||
@ -137,14 +139,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
|
||||
|
||||
@ -94,7 +94,6 @@ class FlightGroupConfigurator:
|
||||
self.flight,
|
||||
self.group,
|
||||
self.mission,
|
||||
self.game.conditions.start_time,
|
||||
self.time,
|
||||
self.game.settings,
|
||||
self.mission_data,
|
||||
|
||||
@ -22,8 +22,6 @@ class HoldPointBuilder(PydcsWaypointBuilder):
|
||||
return
|
||||
push_time = self.flight.flight_plan.push_time
|
||||
self.waypoint.departure_time = push_time
|
||||
loiter.stop_after_time(
|
||||
int((push_time - self.elapsed_mission_time).total_seconds())
|
||||
)
|
||||
loiter.stop_after_time(int((push_time - self.now).total_seconds()))
|
||||
waypoint.add_task(loiter)
|
||||
waypoint.add_task(OptFormation.finger_four_close())
|
||||
|
||||
@ -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,
|
||||
unit_map: UnitMap,
|
||||
) -> None:
|
||||
@ -37,7 +37,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
|
||||
self.unit_map = unit_map
|
||||
|
||||
@ -68,10 +68,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
|
||||
|
||||
|
||||
@ -56,7 +56,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
|
||||
racetrack.stop_after_time(int(loiter_duration.total_seconds()))
|
||||
waypoint.add_task(racetrack)
|
||||
|
||||
|
||||
@ -25,13 +25,13 @@ from game.settings import Settings
|
||||
from game.unitmap import UnitMap
|
||||
from game.utils import pairwise
|
||||
from .baiingress import BaiIngressBuilder
|
||||
from .landingzone import LandingZoneBuilder
|
||||
from .casingress import CasIngressBuilder
|
||||
from .deadingress import DeadIngressBuilder
|
||||
from .default import DefaultWaypointBuilder
|
||||
from .holdpoint import HoldPointBuilder
|
||||
from .joinpoint import JoinPointBuilder
|
||||
from .landingpoint import LandingPointBuilder
|
||||
from .landingzone import LandingZoneBuilder
|
||||
from .ocaaircraftingress import OcaAircraftIngressBuilder
|
||||
from .ocarunwayingress import OcaRunwayIngressBuilder
|
||||
from .pydcswaypointbuilder import PydcsWaypointBuilder, TARGET_WAYPOINTS
|
||||
@ -50,7 +50,6 @@ class WaypointGenerator:
|
||||
flight: Flight,
|
||||
group: FlyingGroup[Any],
|
||||
mission: Mission,
|
||||
turn_start_time: datetime,
|
||||
time: datetime,
|
||||
settings: Settings,
|
||||
mission_data: MissionData,
|
||||
@ -59,7 +58,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
|
||||
@ -150,7 +148,7 @@ class WaypointGenerator:
|
||||
self.group,
|
||||
self.flight,
|
||||
self.mission,
|
||||
self.elapsed_mission_time,
|
||||
self.time,
|
||||
self.mission_data,
|
||||
self.unit_map,
|
||||
)
|
||||
@ -182,12 +180,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
|
||||
@ -127,11 +126,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 ""
|
||||
|
||||
|
||||
|
||||
@ -254,11 +254,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}"
|
||||
@ -583,11 +583,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 ''}"
|
||||
|
||||
|
||||
class SeadTaskPage(KneeboardPage):
|
||||
|
||||
@ -1,11 +1,11 @@
|
||||
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
|
||||
from game.missiongenerator.aircraft.flightdata import FlightData
|
||||
|
||||
from game.runways import RunwayData
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@ -31,8 +31,8 @@ class AwacsInfo(GroupInfo):
|
||||
"""AWACS information for the kneeboard."""
|
||||
|
||||
depature_location: Optional[str]
|
||||
start_time: Optional[timedelta]
|
||||
end_time: Optional[timedelta]
|
||||
start_time: datetime | None
|
||||
end_time: datetime | None
|
||||
|
||||
|
||||
@dataclass
|
||||
@ -41,8 +41,8 @@ class TankerInfo(GroupInfo):
|
||||
|
||||
variant: str
|
||||
tacan: TacanChannel
|
||||
start_time: Optional[timedelta]
|
||||
end_time: Optional[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,6 +4,7 @@ import logging
|
||||
import random
|
||||
from collections.abc import Iterable
|
||||
from dataclasses import dataclass, field
|
||||
from datetime import datetime
|
||||
from typing import Optional, Sequence, TYPE_CHECKING
|
||||
|
||||
from faker import Faker
|
||||
@ -334,7 +335,7 @@ 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:
|
||||
if destination == self.location:
|
||||
logging.warning(
|
||||
f"Attempted to plan relocation of {self} to current location "
|
||||
@ -353,7 +354,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:
|
||||
if self.destination is None:
|
||||
@ -368,9 +369,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:
|
||||
@ -381,7 +382,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."
|
||||
@ -395,7 +396,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:
|
||||
|
||||
@ -902,7 +902,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:
|
||||
@ -580,7 +581,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]
|
||||
@ -595,12 +596,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)
|
||||
|
||||
def split_transfer(self, transfer: TransferOrder, size: int) -> TransferOrder:
|
||||
"""Creates a smaller transfer that is a subset of the original."""
|
||||
@ -672,7 +673,7 @@ class PendingTransfers:
|
||||
self.convoys.disband_all()
|
||||
self.cargo_ships.disband_all()
|
||||
|
||||
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
|
||||
@ -682,7 +683,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:
|
||||
"""
|
||||
|
||||
@ -138,11 +138,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.from_cp.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]:
|
||||
@ -184,7 +182,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()
|
||||
|
||||
@ -194,7 +192,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()
|
||||
@ -381,11 +381,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 PySide6.QtWidgets import (
|
||||
@ -155,13 +156,14 @@ 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:
|
||||
continue
|
||||
for flight in package.flights:
|
||||
if flight.flight_plan.startup_time().total_seconds() < 0:
|
||||
startup = flight.flight_plan.startup_time()
|
||||
if startup < now:
|
||||
packages.append(package)
|
||||
break
|
||||
return packages
|
||||
@ -277,7 +279,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 PySide6.QtCore import (
|
||||
@ -253,16 +252,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 PySide6.QtCore import QItemSelectionModel, QModelIndex, QSize
|
||||
from PySide6.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 game.ato.flight import Flight
|
||||
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()
|
||||
@ -229,6 +233,7 @@ class AirWingTabs(QTabWidget):
|
||||
game_model.ato_model,
|
||||
game_model.blue_air_wing_model,
|
||||
game_model.game.theater,
|
||||
game_model.sim_controller,
|
||||
),
|
||||
"Squadrons",
|
||||
)
|
||||
|
||||
@ -20,6 +20,7 @@ from game.theater import ConflictTheater, ControlPoint
|
||||
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
|
||||
|
||||
|
||||
class PilotDelegate(TwoColumnRowDelegate):
|
||||
@ -134,11 +135,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))
|
||||
@ -194,7 +197,9 @@ class SquadronDialog(QDialog):
|
||||
if destination is None:
|
||||
self.squadron.cancel_relocation()
|
||||
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(
|
||||
|
||||
@ -303,7 +303,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:
|
||||
|
||||
@ -16,4 +16,4 @@ class QFlightItem(QStandardItem):
|
||||
icon = QIcon((AIRCRAFT_ICONS[self.flight.unit_type.dcs_id]))
|
||||
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 PySide6.QtCore import QItemSelection, QTime, Qt, Signal
|
||||
@ -77,7 +76,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)
|
||||
@ -133,11 +132,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
|
||||
@ -151,9 +147,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 PySide6.QtCore import QItemSelectionModel, QSize
|
||||
from PySide6.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()
|
||||
|
||||
@ -57,7 +57,9 @@ class FlightAirfieldDisplay(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}"
|
||||
)
|
||||
|
||||
def set_divert(self, index: int) -> None:
|
||||
old_divert = self.flight.divert
|
||||
|
||||
@ -1,13 +1,11 @@
|
||||
from datetime import timedelta
|
||||
|
||||
from PySide6.QtCore import QItemSelectionModel, QPoint
|
||||
from PySide6.QtGui import QStandardItem, QStandardItemModel
|
||||
from PySide6.QtWidgets import QHeaderView, QTableView
|
||||
|
||||
from game.ato.package import Package
|
||||
from game.ato.flightwaypointtype import FlightWaypointType
|
||||
from game.ato.flightwaypoint import FlightWaypoint
|
||||
from game.ato.flight import Flight
|
||||
from game.ato.flightwaypoint import FlightWaypoint
|
||||
from game.ato.flightwaypointtype import FlightWaypointType
|
||||
from game.ato.package import Package
|
||||
from qt_ui.windows.mission.flight.waypoints.QFlightWaypointItem import QWaypointItem
|
||||
|
||||
|
||||
@ -77,14 +75,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