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 __future__ import annotations
|
||||||
|
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from datetime import timedelta
|
from datetime import datetime
|
||||||
from typing import Iterator, TYPE_CHECKING, Type
|
from typing import Iterator, TYPE_CHECKING, Type
|
||||||
|
|
||||||
from game.ato.flightplans.standard import StandardFlightPlan, StandardLayout
|
from game.ato.flightplans.standard import StandardFlightPlan, StandardLayout
|
||||||
@ -55,12 +55,12 @@ class AirAssaultFlightPlan(StandardFlightPlan[AirAssaultLayout], UiZoneDisplay):
|
|||||||
def tot_waypoint(self) -> FlightWaypoint:
|
def tot_waypoint(self) -> FlightWaypoint:
|
||||||
return self.layout.drop_off
|
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:
|
if waypoint == self.tot_waypoint:
|
||||||
return self.tot
|
return self.tot
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def depart_time_for_waypoint(self, waypoint: FlightWaypoint) -> timedelta | None:
|
def depart_time_for_waypoint(self, waypoint: FlightWaypoint) -> datetime | None:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@ -68,11 +68,11 @@ class AirAssaultFlightPlan(StandardFlightPlan[AirAssaultLayout], UiZoneDisplay):
|
|||||||
return meters(2500)
|
return meters(2500)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def mission_begin_on_station_time(self) -> timedelta | None:
|
def mission_begin_on_station_time(self) -> datetime | None:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def mission_departure_time(self) -> timedelta:
|
def mission_departure_time(self) -> datetime:
|
||||||
return self.package.time_over_target
|
return self.package.time_over_target
|
||||||
|
|
||||||
def ui_zone(self) -> UiZone:
|
def ui_zone(self) -> UiZone:
|
||||||
|
|||||||
@ -2,7 +2,7 @@ from __future__ import annotations
|
|||||||
|
|
||||||
from collections.abc import Iterator
|
from collections.abc import Iterator
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from datetime import timedelta
|
from datetime import datetime
|
||||||
from typing import TYPE_CHECKING, Type
|
from typing import TYPE_CHECKING, Type
|
||||||
|
|
||||||
from game.theater.missiontarget import MissionTarget
|
from game.theater.missiontarget import MissionTarget
|
||||||
@ -67,20 +67,20 @@ class AirliftFlightPlan(StandardFlightPlan[AirliftLayout]):
|
|||||||
# drop-off waypoint.
|
# drop-off waypoint.
|
||||||
return self.layout.drop_off or self.layout.arrival
|
return self.layout.drop_off or self.layout.arrival
|
||||||
|
|
||||||
def tot_for_waypoint(self, waypoint: FlightWaypoint) -> timedelta | None:
|
def tot_for_waypoint(self, waypoint: FlightWaypoint) -> datetime | None:
|
||||||
# TOT planning isn't really useful for transports. They're behind the front
|
# TOT planning isn't really useful for transports. They're behind the front
|
||||||
# lines so no need to wait for escorts or for other missions to complete.
|
# lines so no need to wait for escorts or for other missions to complete.
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def depart_time_for_waypoint(self, waypoint: FlightWaypoint) -> timedelta | None:
|
def depart_time_for_waypoint(self, waypoint: FlightWaypoint) -> datetime | None:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def mission_begin_on_station_time(self) -> timedelta | None:
|
def mission_begin_on_station_time(self) -> datetime | None:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def mission_departure_time(self) -> timedelta:
|
def mission_departure_time(self) -> datetime:
|
||||||
return self.package.time_over_target
|
return self.package.time_over_target
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -2,7 +2,7 @@ from __future__ import annotations
|
|||||||
|
|
||||||
from collections.abc import Iterator
|
from collections.abc import Iterator
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from datetime import timedelta
|
from datetime import datetime
|
||||||
from typing import TYPE_CHECKING, Type
|
from typing import TYPE_CHECKING, Type
|
||||||
|
|
||||||
from .flightplan import FlightPlan, Layout
|
from .flightplan import FlightPlan, Layout
|
||||||
@ -42,20 +42,20 @@ class CustomFlightPlan(FlightPlan[CustomLayout]):
|
|||||||
return waypoint
|
return waypoint
|
||||||
return self.layout.departure
|
return self.layout.departure
|
||||||
|
|
||||||
def tot_for_waypoint(self, waypoint: FlightWaypoint) -> timedelta | None:
|
def tot_for_waypoint(self, waypoint: FlightWaypoint) -> datetime | None:
|
||||||
if waypoint == self.tot_waypoint:
|
if waypoint == self.tot_waypoint:
|
||||||
return self.package.time_over_target
|
return self.package.time_over_target
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def depart_time_for_waypoint(self, waypoint: FlightWaypoint) -> timedelta | None:
|
def depart_time_for_waypoint(self, waypoint: FlightWaypoint) -> datetime | None:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def mission_begin_on_station_time(self) -> timedelta | None:
|
def mission_begin_on_station_time(self) -> datetime | None:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def mission_departure_time(self) -> timedelta:
|
def mission_departure_time(self) -> datetime:
|
||||||
return self.package.time_over_target
|
return self.package.time_over_target
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -2,7 +2,7 @@ from __future__ import annotations
|
|||||||
|
|
||||||
from collections.abc import Iterator
|
from collections.abc import Iterator
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from datetime import timedelta
|
from datetime import datetime
|
||||||
from typing import TYPE_CHECKING, Type
|
from typing import TYPE_CHECKING, Type
|
||||||
|
|
||||||
from game.utils import feet
|
from game.utils import feet
|
||||||
@ -37,20 +37,20 @@ class FerryFlightPlan(StandardFlightPlan[FerryLayout]):
|
|||||||
def tot_waypoint(self) -> FlightWaypoint:
|
def tot_waypoint(self) -> FlightWaypoint:
|
||||||
return self.layout.arrival
|
return self.layout.arrival
|
||||||
|
|
||||||
def tot_for_waypoint(self, waypoint: FlightWaypoint) -> timedelta | None:
|
def tot_for_waypoint(self, waypoint: FlightWaypoint) -> datetime | None:
|
||||||
# TOT planning isn't really useful for ferries. They're behind the front
|
# TOT planning isn't really useful for ferries. They're behind the front
|
||||||
# lines so no need to wait for escorts or for other missions to complete.
|
# lines so no need to wait for escorts or for other missions to complete.
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def depart_time_for_waypoint(self, waypoint: FlightWaypoint) -> timedelta | None:
|
def depart_time_for_waypoint(self, waypoint: FlightWaypoint) -> datetime | None:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def mission_begin_on_station_time(self) -> timedelta | None:
|
def mission_begin_on_station_time(self) -> datetime | None:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def mission_departure_time(self) -> timedelta:
|
def mission_departure_time(self) -> datetime:
|
||||||
return self.package.time_over_target
|
return self.package.time_over_target
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -11,7 +11,7 @@ import math
|
|||||||
from abc import ABC, abstractmethod
|
from abc import ABC, abstractmethod
|
||||||
from collections.abc import Iterator
|
from collections.abc import Iterator
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from datetime import timedelta
|
from datetime import datetime, timedelta
|
||||||
from functools import cached_property
|
from functools import cached_property
|
||||||
from typing import Any, Generic, TYPE_CHECKING, TypeGuard, TypeVar
|
from typing import Any, Generic, TYPE_CHECKING, TypeGuard, TypeVar
|
||||||
|
|
||||||
@ -149,7 +149,7 @@ class FlightPlan(ABC, Generic[LayoutT]):
|
|||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def tot(self) -> timedelta:
|
def tot(self) -> datetime:
|
||||||
return self.package.time_over_target + self.tot_offset
|
return self.package.time_over_target + self.tot_offset
|
||||||
|
|
||||||
@cached_property
|
@cached_property
|
||||||
@ -215,7 +215,13 @@ class FlightPlan(ABC, Generic[LayoutT]):
|
|||||||
|
|
||||||
for previous_waypoint, waypoint in self.edges(until=destination):
|
for previous_waypoint, waypoint in self.edges(until=destination):
|
||||||
total += self.travel_time_between_waypoints(previous_waypoint, waypoint)
|
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(
|
def travel_time_between_waypoints(
|
||||||
self, a: FlightWaypoint, b: FlightWaypoint
|
self, a: FlightWaypoint, b: FlightWaypoint
|
||||||
@ -224,10 +230,10 @@ class FlightPlan(ABC, Generic[LayoutT]):
|
|||||||
a.position, b.position, self.speed_between_waypoints(a, b)
|
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
|
raise NotImplementedError
|
||||||
|
|
||||||
def depart_time_for_waypoint(self, waypoint: FlightWaypoint) -> timedelta | None:
|
def depart_time_for_waypoint(self, waypoint: FlightWaypoint) -> datetime | None:
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
def request_escort_at(self) -> FlightWaypoint | None:
|
def request_escort_at(self) -> FlightWaypoint | None:
|
||||||
@ -250,34 +256,20 @@ class FlightPlan(ABC, Generic[LayoutT]):
|
|||||||
if waypoint == end:
|
if waypoint == end:
|
||||||
return
|
return
|
||||||
|
|
||||||
def takeoff_time(self) -> timedelta:
|
def takeoff_time(self) -> datetime:
|
||||||
return self.tot - self._travel_time_to_waypoint(self.tot_waypoint)
|
return self.tot - self._travel_time_to_waypoint(self.tot_waypoint)
|
||||||
|
|
||||||
def startup_time(self) -> timedelta:
|
def minimum_duration_from_start_to_tot(self) -> timedelta:
|
||||||
start_time = (
|
return (
|
||||||
self.takeoff_time() - self.estimate_startup() - self.estimate_ground_ops()
|
self._travel_time_to_waypoint(self.tot_waypoint)
|
||||||
|
+ self.estimate_startup()
|
||||||
|
+ self.estimate_ground_ops()
|
||||||
)
|
)
|
||||||
|
|
||||||
# In case FP math has given us some barely below zero time, round to
|
def startup_time(self) -> datetime:
|
||||||
# zero.
|
return (
|
||||||
if math.isclose(start_time.total_seconds(), 0):
|
self.takeoff_time() - self.estimate_startup() - self.estimate_ground_ops()
|
||||||
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 estimate_startup(self) -> timedelta:
|
def estimate_startup(self) -> timedelta:
|
||||||
if self.flight.start_type is StartType.COLD:
|
if self.flight.start_type is StartType.COLD:
|
||||||
@ -298,7 +290,7 @@ class FlightPlan(ABC, Generic[LayoutT]):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
@abstractmethod
|
@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.
|
"""The time that the mission is first on-station.
|
||||||
|
|
||||||
Not all mission types will have a time when they can be considered 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
|
@property
|
||||||
def mission_departure_time(self) -> timedelta:
|
def mission_departure_time(self) -> datetime:
|
||||||
"""The time that the mission is complete and the flight RTBs."""
|
"""The time that the mission is complete and the flight RTBs."""
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
|
|||||||
@ -2,7 +2,7 @@ from __future__ import annotations
|
|||||||
|
|
||||||
from abc import ABC, abstractmethod
|
from abc import ABC, abstractmethod
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from datetime import timedelta
|
from datetime import datetime, timedelta
|
||||||
from functools import cached_property
|
from functools import cached_property
|
||||||
from typing import Any, TYPE_CHECKING, TypeGuard
|
from typing import Any, TYPE_CHECKING, TypeGuard
|
||||||
|
|
||||||
@ -73,15 +73,15 @@ class FormationFlightPlan(LoiterFlightPlan, ABC):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def join_time(self) -> timedelta:
|
def join_time(self) -> datetime:
|
||||||
...
|
...
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def split_time(self) -> timedelta:
|
def split_time(self) -> datetime:
|
||||||
...
|
...
|
||||||
|
|
||||||
def tot_for_waypoint(self, waypoint: FlightWaypoint) -> timedelta | None:
|
def tot_for_waypoint(self, waypoint: FlightWaypoint) -> datetime | None:
|
||||||
if waypoint == self.layout.join:
|
if waypoint == self.layout.join:
|
||||||
return self.join_time
|
return self.join_time
|
||||||
elif waypoint == self.layout.split:
|
elif waypoint == self.layout.split:
|
||||||
@ -89,7 +89,7 @@ class FormationFlightPlan(LoiterFlightPlan, ABC):
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def push_time(self) -> timedelta:
|
def push_time(self) -> datetime:
|
||||||
return self.join_time - TravelTime.between_points(
|
return self.join_time - TravelTime.between_points(
|
||||||
self.layout.hold.position,
|
self.layout.hold.position,
|
||||||
self.layout.join.position,
|
self.layout.join.position,
|
||||||
@ -97,11 +97,11 @@ class FormationFlightPlan(LoiterFlightPlan, ABC):
|
|||||||
)
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def mission_begin_on_station_time(self) -> timedelta | None:
|
def mission_begin_on_station_time(self) -> datetime | None:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def mission_departure_time(self) -> timedelta:
|
def mission_departure_time(self) -> datetime:
|
||||||
return self.split_time
|
return self.split_time
|
||||||
|
|
||||||
@self_type_guard
|
@self_type_guard
|
||||||
|
|||||||
@ -3,7 +3,7 @@ from __future__ import annotations
|
|||||||
from abc import ABC
|
from abc import ABC
|
||||||
from collections.abc import Iterator
|
from collections.abc import Iterator
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from datetime import timedelta
|
from datetime import datetime, timedelta
|
||||||
from typing import TYPE_CHECKING, TypeVar
|
from typing import TYPE_CHECKING, TypeVar
|
||||||
|
|
||||||
from dcs import Point
|
from dcs import Point
|
||||||
@ -91,14 +91,14 @@ class FormationAttackFlightPlan(FormationFlightPlan, ABC):
|
|||||||
return total
|
return total
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def join_time(self) -> timedelta:
|
def join_time(self) -> datetime:
|
||||||
travel_time = self.travel_time_between_waypoints(
|
travel_time = self.travel_time_between_waypoints(
|
||||||
self.layout.join, self.layout.ingress
|
self.layout.join, self.layout.ingress
|
||||||
)
|
)
|
||||||
return self.ingress_time - travel_time
|
return self.ingress_time - travel_time
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def split_time(self) -> timedelta:
|
def split_time(self) -> datetime:
|
||||||
travel_time_ingress = self.travel_time_between_waypoints(
|
travel_time_ingress = self.travel_time_between_waypoints(
|
||||||
self.layout.ingress, self.target_area_waypoint
|
self.layout.ingress, self.target_area_waypoint
|
||||||
)
|
)
|
||||||
@ -115,14 +115,14 @@ class FormationAttackFlightPlan(FormationFlightPlan, ABC):
|
|||||||
)
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def ingress_time(self) -> timedelta:
|
def ingress_time(self) -> datetime:
|
||||||
tot = self.tot
|
tot = self.tot
|
||||||
travel_time = self.travel_time_between_waypoints(
|
travel_time = self.travel_time_between_waypoints(
|
||||||
self.layout.ingress, self.target_area_waypoint
|
self.layout.ingress, self.target_area_waypoint
|
||||||
)
|
)
|
||||||
return tot - travel_time
|
return tot - travel_time
|
||||||
|
|
||||||
def tot_for_waypoint(self, waypoint: FlightWaypoint) -> timedelta | None:
|
def tot_for_waypoint(self, waypoint: FlightWaypoint) -> datetime | None:
|
||||||
if waypoint == self.layout.ingress:
|
if waypoint == self.layout.ingress:
|
||||||
return self.ingress_time
|
return self.ingress_time
|
||||||
elif waypoint in self.layout.targets:
|
elif waypoint in self.layout.targets:
|
||||||
|
|||||||
@ -2,7 +2,7 @@ from __future__ import annotations
|
|||||||
|
|
||||||
from abc import ABC, abstractmethod
|
from abc import ABC, abstractmethod
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from datetime import timedelta
|
from datetime import datetime, timedelta
|
||||||
from typing import Any, TYPE_CHECKING, TypeGuard
|
from typing import Any, TYPE_CHECKING, TypeGuard
|
||||||
|
|
||||||
from game.typeguard import self_type_guard
|
from game.typeguard import self_type_guard
|
||||||
@ -25,10 +25,10 @@ class LoiterFlightPlan(StandardFlightPlan[Any], ABC):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def push_time(self) -> timedelta:
|
def push_time(self) -> datetime:
|
||||||
...
|
...
|
||||||
|
|
||||||
def depart_time_for_waypoint(self, waypoint: FlightWaypoint) -> timedelta | None:
|
def depart_time_for_waypoint(self, waypoint: FlightWaypoint) -> datetime | None:
|
||||||
if waypoint == self.layout.hold:
|
if waypoint == self.layout.hold:
|
||||||
return self.push_time
|
return self.push_time
|
||||||
return None
|
return None
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from datetime import timedelta
|
from datetime import datetime, timedelta
|
||||||
from typing import Type
|
from typing import Type
|
||||||
|
|
||||||
from dcs import Point
|
from dcs import Point
|
||||||
@ -39,7 +39,7 @@ class PackageRefuelingFlightPlan(RefuelingFlightPlan):
|
|||||||
)
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def patrol_start_time(self) -> timedelta:
|
def patrol_start_time(self) -> datetime:
|
||||||
altitude = self.flight.unit_type.patrol_altitude
|
altitude = self.flight.unit_type.patrol_altitude
|
||||||
|
|
||||||
if altitude is None:
|
if altitude is None:
|
||||||
|
|||||||
@ -3,7 +3,7 @@ from __future__ import annotations
|
|||||||
from abc import ABC, abstractmethod
|
from abc import ABC, abstractmethod
|
||||||
from collections.abc import Iterator
|
from collections.abc import Iterator
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from datetime import timedelta
|
from datetime import datetime, timedelta
|
||||||
from typing import Any, TYPE_CHECKING, TypeGuard, TypeVar
|
from typing import Any, TYPE_CHECKING, TypeGuard, TypeVar
|
||||||
|
|
||||||
from game.ato.flightplans.standard import StandardFlightPlan, StandardLayout
|
from game.ato.flightplans.standard import StandardFlightPlan, StandardLayout
|
||||||
@ -61,22 +61,22 @@ class PatrollingFlightPlan(StandardFlightPlan[LayoutT], UiZoneDisplay, ABC):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def patrol_start_time(self) -> timedelta:
|
def patrol_start_time(self) -> datetime:
|
||||||
return self.package.time_over_target
|
return self.package.time_over_target
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def patrol_end_time(self) -> timedelta:
|
def patrol_end_time(self) -> datetime:
|
||||||
# TODO: This is currently wrong for CAS.
|
# TODO: This is currently wrong for CAS.
|
||||||
# CAS missions end when they're winchester or bingo. We need to
|
# CAS missions end when they're winchester or bingo. We need to
|
||||||
# configure push tasks for the escorts rather than relying on timing.
|
# configure push tasks for the escorts rather than relying on timing.
|
||||||
return self.patrol_start_time + self.patrol_duration
|
return self.patrol_start_time + self.patrol_duration
|
||||||
|
|
||||||
def tot_for_waypoint(self, waypoint: FlightWaypoint) -> timedelta | None:
|
def tot_for_waypoint(self, waypoint: FlightWaypoint) -> datetime | None:
|
||||||
if waypoint == self.layout.patrol_start:
|
if waypoint == self.layout.patrol_start:
|
||||||
return self.patrol_start_time
|
return self.patrol_start_time
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def depart_time_for_waypoint(self, waypoint: FlightWaypoint) -> timedelta | None:
|
def depart_time_for_waypoint(self, waypoint: FlightWaypoint) -> datetime | None:
|
||||||
if waypoint == self.layout.patrol_end:
|
if waypoint == self.layout.patrol_end:
|
||||||
return self.patrol_end_time
|
return self.patrol_end_time
|
||||||
return None
|
return None
|
||||||
@ -90,11 +90,11 @@ class PatrollingFlightPlan(StandardFlightPlan[LayoutT], UiZoneDisplay, ABC):
|
|||||||
return self.layout.patrol_start
|
return self.layout.patrol_start
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def mission_begin_on_station_time(self) -> timedelta:
|
def mission_begin_on_station_time(self) -> datetime:
|
||||||
return self.patrol_start_time
|
return self.patrol_start_time
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def mission_departure_time(self) -> timedelta:
|
def mission_departure_time(self) -> datetime:
|
||||||
return self.patrol_end_time
|
return self.patrol_end_time
|
||||||
|
|
||||||
@self_type_guard
|
@self_type_guard
|
||||||
|
|||||||
@ -2,7 +2,7 @@ from __future__ import annotations
|
|||||||
|
|
||||||
from collections.abc import Iterator
|
from collections.abc import Iterator
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from datetime import timedelta
|
from datetime import datetime
|
||||||
from typing import TYPE_CHECKING, Type
|
from typing import TYPE_CHECKING, Type
|
||||||
|
|
||||||
from game.utils import feet
|
from game.utils import feet
|
||||||
@ -43,19 +43,19 @@ class RtbFlightPlan(StandardFlightPlan[RtbLayout]):
|
|||||||
def tot_waypoint(self) -> FlightWaypoint:
|
def tot_waypoint(self) -> FlightWaypoint:
|
||||||
return self.layout.abort_location
|
return self.layout.abort_location
|
||||||
|
|
||||||
def tot_for_waypoint(self, waypoint: FlightWaypoint) -> timedelta | None:
|
def tot_for_waypoint(self, waypoint: FlightWaypoint) -> datetime | None:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def depart_time_for_waypoint(self, waypoint: FlightWaypoint) -> timedelta | None:
|
def depart_time_for_waypoint(self, waypoint: FlightWaypoint) -> datetime | None:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def mission_begin_on_station_time(self) -> timedelta | None:
|
def mission_begin_on_station_time(self) -> datetime | None:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def mission_departure_time(self) -> timedelta:
|
def mission_departure_time(self) -> datetime:
|
||||||
return timedelta()
|
return self.tot
|
||||||
|
|
||||||
|
|
||||||
class Builder(IBuilder[RtbFlightPlan, RtbLayout]):
|
class Builder(IBuilder[RtbFlightPlan, RtbLayout]):
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from datetime import timedelta
|
from datetime import datetime, timedelta
|
||||||
from typing import Iterator, Type
|
from typing import Iterator, Type
|
||||||
|
|
||||||
from game.ato.flightplans.ibuilder import IBuilder
|
from game.ato.flightplans.ibuilder import IBuilder
|
||||||
@ -37,19 +37,27 @@ class RecoveryTankerFlightPlan(StandardFlightPlan[RecoveryTankerLayout]):
|
|||||||
return self.layout.recovery_ship
|
return self.layout.recovery_ship
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def mission_begin_on_station_time(self) -> timedelta:
|
def mission_begin_on_station_time(self) -> datetime:
|
||||||
return self.package.time_over_target
|
return self.package.time_over_target
|
||||||
|
|
||||||
@property
|
@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)
|
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:
|
if waypoint == self.tot_waypoint:
|
||||||
return self.tot
|
return self.tot
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def depart_time_for_waypoint(self, waypoint: FlightWaypoint) -> timedelta | None:
|
def depart_time_for_waypoint(self, waypoint: FlightWaypoint) -> datetime | None:
|
||||||
if waypoint == self.tot_waypoint:
|
if waypoint == self.tot_waypoint:
|
||||||
return self.mission_departure_time
|
return self.mission_departure_time
|
||||||
return None
|
return None
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from datetime import timedelta
|
from datetime import datetime, timedelta
|
||||||
from typing import Iterator, TYPE_CHECKING, Type
|
from typing import Iterator, TYPE_CHECKING, Type
|
||||||
|
|
||||||
from dcs import Point
|
from dcs import Point
|
||||||
@ -59,30 +59,30 @@ class SweepFlightPlan(LoiterFlightPlan):
|
|||||||
return -self.lead_time
|
return -self.lead_time
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def sweep_start_time(self) -> timedelta:
|
def sweep_start_time(self) -> datetime:
|
||||||
travel_time = self.travel_time_between_waypoints(
|
travel_time = self.travel_time_between_waypoints(
|
||||||
self.layout.sweep_start, self.layout.sweep_end
|
self.layout.sweep_start, self.layout.sweep_end
|
||||||
)
|
)
|
||||||
return self.sweep_end_time - travel_time
|
return self.sweep_end_time - travel_time
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def sweep_end_time(self) -> timedelta:
|
def sweep_end_time(self) -> datetime:
|
||||||
return self.tot
|
return self.tot
|
||||||
|
|
||||||
def tot_for_waypoint(self, waypoint: FlightWaypoint) -> timedelta | None:
|
def tot_for_waypoint(self, waypoint: FlightWaypoint) -> datetime | None:
|
||||||
if waypoint == self.layout.sweep_start:
|
if waypoint == self.layout.sweep_start:
|
||||||
return self.sweep_start_time
|
return self.sweep_start_time
|
||||||
if waypoint == self.layout.sweep_end:
|
if waypoint == self.layout.sweep_end:
|
||||||
return self.sweep_end_time
|
return self.sweep_end_time
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def depart_time_for_waypoint(self, waypoint: FlightWaypoint) -> timedelta | None:
|
def depart_time_for_waypoint(self, waypoint: FlightWaypoint) -> datetime | None:
|
||||||
if waypoint == self.layout.hold:
|
if waypoint == self.layout.hold:
|
||||||
return self.push_time
|
return self.push_time
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def push_time(self) -> timedelta:
|
def push_time(self) -> datetime:
|
||||||
return self.sweep_end_time - TravelTime.between_points(
|
return self.sweep_end_time - TravelTime.between_points(
|
||||||
self.layout.hold.position,
|
self.layout.hold.position,
|
||||||
self.layout.sweep_end.position,
|
self.layout.sweep_end.position,
|
||||||
@ -90,10 +90,10 @@ class SweepFlightPlan(LoiterFlightPlan):
|
|||||||
)
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def mission_begin_on_station_time(self) -> timedelta | None:
|
def mission_begin_on_station_time(self) -> datetime | None:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def mission_departure_time(self) -> timedelta:
|
def mission_departure_time(self) -> datetime:
|
||||||
return self.sweep_end_time
|
return self.sweep_end_time
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -3,7 +3,7 @@ from __future__ import annotations
|
|||||||
import random
|
import random
|
||||||
from collections.abc import Iterator
|
from collections.abc import Iterator
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from datetime import timedelta
|
from datetime import datetime, timedelta
|
||||||
from typing import TYPE_CHECKING, Type
|
from typing import TYPE_CHECKING, Type
|
||||||
|
|
||||||
from game.utils import Distance, Speed, feet
|
from game.utils import Distance, Speed, feet
|
||||||
@ -68,20 +68,20 @@ class TarCapFlightPlan(PatrollingFlightPlan[TarCapLayout]):
|
|||||||
def tot_offset(self) -> timedelta:
|
def tot_offset(self) -> timedelta:
|
||||||
return -self.lead_time
|
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:
|
if waypoint == self.layout.patrol_end:
|
||||||
return self.patrol_end_time
|
return self.patrol_end_time
|
||||||
return super().depart_time_for_waypoint(waypoint)
|
return super().depart_time_for_waypoint(waypoint)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def patrol_start_time(self) -> timedelta:
|
def patrol_start_time(self) -> datetime:
|
||||||
start = self.package.escort_start_time
|
start = self.package.escort_start_time
|
||||||
if start is not None:
|
if start is not None:
|
||||||
return start + self.tot_offset
|
return start + self.tot_offset
|
||||||
return self.tot
|
return self.tot
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def patrol_end_time(self) -> timedelta:
|
def patrol_end_time(self) -> datetime:
|
||||||
end = self.package.escort_end_time
|
end = self.package.escort_end_time
|
||||||
if end is not None:
|
if end is not None:
|
||||||
return end
|
return end
|
||||||
|
|||||||
@ -35,7 +35,6 @@ class Uninitialized(FlightState):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def description(self) -> str:
|
def description(self) -> str:
|
||||||
delay = self.flight.flight_plan.startup_time()
|
|
||||||
if self.flight.start_type is StartType.COLD:
|
if self.flight.start_type is StartType.COLD:
|
||||||
action = "Starting up"
|
action = "Starting up"
|
||||||
elif self.flight.start_type is StartType.WARM:
|
elif self.flight.start_type is StartType.WARM:
|
||||||
@ -46,4 +45,4 @@ class Uninitialized(FlightState):
|
|||||||
action = "In flight"
|
action = "In flight"
|
||||||
else:
|
else:
|
||||||
raise ValueError(f"Unhandled StartType: {self.flight.start_type}")
|
raise ValueError(f"Unhandled StartType: {self.flight.start_type}")
|
||||||
return f"{action} in {delay}"
|
return f"{action} at {self.flight.flight_plan.startup_time():%H:%M:%S}"
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
from datetime import timedelta
|
from datetime import datetime
|
||||||
from typing import Literal, TYPE_CHECKING
|
from typing import Literal, TYPE_CHECKING
|
||||||
|
|
||||||
from dcs import Point
|
from dcs import Point
|
||||||
@ -43,8 +43,8 @@ class FlightWaypoint:
|
|||||||
# generation). We do it late so that we don't need to propagate changes
|
# generation). We do it late so that we don't need to propagate changes
|
||||||
# to waypoint times whenever the player alters the package TOT or the
|
# to waypoint times whenever the player alters the package TOT or the
|
||||||
# flight's offset in the UI.
|
# flight's offset in the UI.
|
||||||
tot: timedelta | None = None
|
tot: datetime | None = None
|
||||||
departure_time: timedelta | None = None
|
departure_time: datetime | None = None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def x(self) -> float:
|
def x(self) -> float:
|
||||||
|
|||||||
@ -2,7 +2,7 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import logging
|
import logging
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
from datetime import timedelta
|
from datetime import datetime
|
||||||
from typing import Dict, Optional, TYPE_CHECKING
|
from typing import Dict, Optional, TYPE_CHECKING
|
||||||
|
|
||||||
from game.db import Database
|
from game.db import Database
|
||||||
@ -33,8 +33,11 @@ class Package:
|
|||||||
self.auto_asap = auto_asap
|
self.auto_asap = auto_asap
|
||||||
self.flights: list[Flight] = []
|
self.flights: list[Flight] = []
|
||||||
|
|
||||||
# Desired TOT as an offset from mission start.
|
# Desired TOT as an offset from mission start. Obviously datetime.min is bogus,
|
||||||
self.time_over_target: timedelta = timedelta()
|
# but it's going to be replaced by whatever is scheduling the package very soon.
|
||||||
|
# TODO: Constructor should maybe take the current time and use that to preserve
|
||||||
|
# the old behavior?
|
||||||
|
self.time_over_target: datetime = datetime.min
|
||||||
self.waypoints: PackageWaypoints | None = None
|
self.waypoints: PackageWaypoints | None = None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@ -62,7 +65,7 @@ class Package:
|
|||||||
# TODO: Should depend on the type of escort.
|
# TODO: Should depend on the type of escort.
|
||||||
# SEAD might be able to leave before CAP.
|
# SEAD might be able to leave before CAP.
|
||||||
@property
|
@property
|
||||||
def escort_start_time(self) -> Optional[timedelta]:
|
def escort_start_time(self) -> datetime | None:
|
||||||
times = []
|
times = []
|
||||||
for flight in self.flights:
|
for flight in self.flights:
|
||||||
waypoint = flight.flight_plan.request_escort_at()
|
waypoint = flight.flight_plan.request_escort_at()
|
||||||
@ -81,7 +84,7 @@ class Package:
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def escort_end_time(self) -> Optional[timedelta]:
|
def escort_end_time(self) -> datetime | None:
|
||||||
times = []
|
times = []
|
||||||
for flight in self.flights:
|
for flight in self.flights:
|
||||||
waypoint = flight.flight_plan.dismiss_escort_at()
|
waypoint = flight.flight_plan.dismiss_escort_at()
|
||||||
@ -103,7 +106,7 @@ class Package:
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def mission_departure_time(self) -> Optional[timedelta]:
|
def mission_departure_time(self) -> datetime | None:
|
||||||
times = []
|
times = []
|
||||||
for flight in self.flights:
|
for flight in self.flights:
|
||||||
times.append(flight.flight_plan.mission_departure_time)
|
times.append(flight.flight_plan.mission_departure_time)
|
||||||
@ -111,8 +114,8 @@ class Package:
|
|||||||
return max(times)
|
return max(times)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def set_tot_asap(self) -> None:
|
def set_tot_asap(self, now: datetime) -> None:
|
||||||
self.time_over_target = TotEstimator(self).earliest_tot()
|
self.time_over_target = TotEstimator(self).earliest_tot(now)
|
||||||
|
|
||||||
def add_flight(self, flight: Flight) -> None:
|
def add_flight(self, flight: Flight) -> None:
|
||||||
"""Adds a flight to the package."""
|
"""Adds a flight to the package."""
|
||||||
|
|||||||
@ -1,7 +1,6 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import math
|
from datetime import datetime, timedelta
|
||||||
from datetime import timedelta
|
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
from dcs.mapping import Point
|
from dcs.mapping import Point
|
||||||
@ -56,44 +55,24 @@ class TotEstimator:
|
|||||||
def __init__(self, package: Package) -> None:
|
def __init__(self, package: Package) -> None:
|
||||||
self.package = package
|
self.package = package
|
||||||
|
|
||||||
def earliest_tot(self) -> timedelta:
|
def earliest_tot(self, now: datetime) -> datetime:
|
||||||
if not self.package.flights:
|
if not self.package.flights:
|
||||||
return timedelta(0)
|
return now
|
||||||
|
|
||||||
earliest_tot = max(
|
return max(self.earliest_tot_for_flight(f, now) for f in self.package.flights)
|
||||||
(self.earliest_tot_for_flight(f) for f in self.package.flights)
|
|
||||||
)
|
|
||||||
|
|
||||||
# Trim microseconds. DCS doesn't handle sub-second resolution for tasks,
|
|
||||||
# and they're not interesting from a mission planning perspective so we
|
|
||||||
# don't want them in the UI.
|
|
||||||
#
|
|
||||||
# Round up so we don't get negative start times.
|
|
||||||
return timedelta(seconds=math.ceil(earliest_tot.total_seconds()))
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def earliest_tot_for_flight(flight: Flight) -> timedelta:
|
def earliest_tot_for_flight(flight: Flight, now: datetime) -> datetime:
|
||||||
"""Estimate the fastest time from mission start to the target position.
|
"""Estimate the earliest time the flight can reach the target position.
|
||||||
|
|
||||||
For BARCAP flights, this is time to the racetrack start. This ensures that
|
The interpretation of the TOT depends on the flight plan type. See the various
|
||||||
they are on station at the same time any other package members reach
|
FlightPlan implementations for details.
|
||||||
their ingress point.
|
|
||||||
|
|
||||||
For other mission types this is the time to the mission target.
|
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
flight: The flight to get the earliest TOT time for.
|
flight: The flight to get the earliest TOT for.
|
||||||
|
now: The current mission time.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
The earliest possible TOT for the given flight in seconds. Returns 0
|
The earliest possible TOT for the given flight.
|
||||||
if an ingress point cannot be found.
|
|
||||||
"""
|
"""
|
||||||
# Clear the TOT, calculate the startup time. Negating the result gives
|
return now + flight.flight_plan.minimum_duration_from_start_to_tot()
|
||||||
# the earliest possible start time.
|
|
||||||
orig_tot = flight.package.time_over_target
|
|
||||||
try:
|
|
||||||
flight.package.time_over_target = timedelta()
|
|
||||||
time = flight.flight_plan.startup_time()
|
|
||||||
finally:
|
|
||||||
flight.package.time_over_target = orig_tot
|
|
||||||
return -time
|
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from datetime import datetime
|
||||||
from typing import Any, Optional, TYPE_CHECKING
|
from typing import Any, Optional, TYPE_CHECKING
|
||||||
|
|
||||||
from faker import Faker
|
from faker import Faker
|
||||||
@ -181,9 +182,9 @@ class Coalition:
|
|||||||
with logged_duration("Procurement of airlift assets"):
|
with logged_duration("Procurement of airlift assets"):
|
||||||
self.transfers.order_airlift_assets()
|
self.transfers.order_airlift_assets()
|
||||||
with logged_duration("Transport planning"):
|
with logged_duration("Transport planning"):
|
||||||
self.transfers.plan_transports()
|
self.transfers.plan_transports(self.game.conditions.start_time)
|
||||||
|
|
||||||
self.plan_missions()
|
self.plan_missions(self.game.conditions.start_time)
|
||||||
self.plan_procurement()
|
self.plan_procurement()
|
||||||
|
|
||||||
def refund_outstanding_orders(self) -> None:
|
def refund_outstanding_orders(self) -> None:
|
||||||
@ -199,16 +200,16 @@ class Coalition:
|
|||||||
for squadron in self.air_wing.iter_squadrons():
|
for squadron in self.air_wing.iter_squadrons():
|
||||||
squadron.refund_orders()
|
squadron.refund_orders()
|
||||||
|
|
||||||
def plan_missions(self) -> None:
|
def plan_missions(self, now: datetime) -> None:
|
||||||
color = "Blue" if self.player else "Red"
|
color = "Blue" if self.player else "Red"
|
||||||
with MultiEventTracer() as tracer:
|
with MultiEventTracer() as tracer:
|
||||||
with tracer.trace(f"{color} mission planning"):
|
with tracer.trace(f"{color} mission planning"):
|
||||||
with tracer.trace(f"{color} mission identification"):
|
with tracer.trace(f"{color} mission identification"):
|
||||||
TheaterCommander(self.game, self.player).plan_missions(tracer)
|
TheaterCommander(self.game, self.player).plan_missions(now, tracer)
|
||||||
with tracer.trace(f"{color} mission scheduling"):
|
with tracer.trace(f"{color} mission scheduling"):
|
||||||
MissionScheduler(
|
MissionScheduler(
|
||||||
self, self.game.settings.desired_player_mission_duration
|
self, self.game.settings.desired_player_mission_duration
|
||||||
).schedule_missions()
|
).schedule_missions(now)
|
||||||
|
|
||||||
def plan_procurement(self) -> None:
|
def plan_procurement(self) -> None:
|
||||||
# The first turn needs to buy a *lot* of aircraft to fill CAPs, so it gets much
|
# The first turn needs to buy a *lot* of aircraft to fill CAPs, so it gets much
|
||||||
|
|||||||
@ -3,12 +3,12 @@ from __future__ import annotations
|
|||||||
import logging
|
import logging
|
||||||
import random
|
import random
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
from datetime import timedelta
|
from datetime import datetime, timedelta
|
||||||
from typing import Iterator, Dict, TYPE_CHECKING
|
from typing import Iterator, TYPE_CHECKING
|
||||||
|
|
||||||
from game.theater import MissionTarget
|
|
||||||
from game.ato.flighttype import FlightType
|
from game.ato.flighttype import FlightType
|
||||||
from game.ato.traveltime import TotEstimator
|
from game.ato.traveltime import TotEstimator
|
||||||
|
from game.theater import MissionTarget
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from game.coalition import Coalition
|
from game.coalition import Coalition
|
||||||
@ -19,7 +19,7 @@ class MissionScheduler:
|
|||||||
self.coalition = coalition
|
self.coalition = coalition
|
||||||
self.desired_mission_length = desired_mission_length
|
self.desired_mission_length = desired_mission_length
|
||||||
|
|
||||||
def schedule_missions(self) -> None:
|
def schedule_missions(self, now: datetime) -> None:
|
||||||
"""Identifies and plans mission for the turn."""
|
"""Identifies and plans mission for the turn."""
|
||||||
|
|
||||||
def start_time_generator(
|
def start_time_generator(
|
||||||
@ -35,7 +35,7 @@ class MissionScheduler:
|
|||||||
FlightType.TARCAP,
|
FlightType.TARCAP,
|
||||||
}
|
}
|
||||||
|
|
||||||
previous_cap_end_time: Dict[MissionTarget, timedelta] = defaultdict(timedelta)
|
previous_cap_end_time: dict[MissionTarget, datetime] = defaultdict(now.replace)
|
||||||
non_dca_packages = [
|
non_dca_packages = [
|
||||||
p for p in self.coalition.ato.packages if p.primary_task not in dca_types
|
p for p in self.coalition.ato.packages if p.primary_task not in dca_types
|
||||||
]
|
]
|
||||||
@ -47,7 +47,7 @@ class MissionScheduler:
|
|||||||
margin=5 * 60,
|
margin=5 * 60,
|
||||||
)
|
)
|
||||||
for package in self.coalition.ato.packages:
|
for package in self.coalition.ato.packages:
|
||||||
tot = TotEstimator(package).earliest_tot()
|
tot = TotEstimator(package).earliest_tot(now)
|
||||||
if package.primary_task in dca_types:
|
if package.primary_task in dca_types:
|
||||||
previous_end_time = previous_cap_end_time[package.target]
|
previous_end_time = previous_cap_end_time[package.target]
|
||||||
if tot > previous_end_time:
|
if tot > previous_end_time:
|
||||||
@ -65,7 +65,7 @@ class MissionScheduler:
|
|||||||
continue
|
continue
|
||||||
previous_cap_end_time[package.target] = departure_time
|
previous_cap_end_time[package.target] = departure_time
|
||||||
elif package.auto_asap:
|
elif package.auto_asap:
|
||||||
package.set_tot_asap()
|
package.set_tot_asap(now)
|
||||||
else:
|
else:
|
||||||
# But other packages should be spread out a bit. Note that take
|
# But other packages should be spread out a bit. Note that take
|
||||||
# times are delayed, but all aircraft will become active at
|
# times are delayed, but all aircraft will become active at
|
||||||
|
|||||||
@ -2,6 +2,7 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import logging
|
import logging
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
|
from datetime import datetime
|
||||||
from typing import Dict, Iterable, Optional, Set, TYPE_CHECKING
|
from typing import Dict, Iterable, Optional, Set, TYPE_CHECKING
|
||||||
|
|
||||||
from game.ato.airtaaskingorder import AirTaskingOrder
|
from game.ato.airtaaskingorder import AirTaskingOrder
|
||||||
@ -132,6 +133,7 @@ class PackageFulfiller:
|
|||||||
self,
|
self,
|
||||||
mission: ProposedMission,
|
mission: ProposedMission,
|
||||||
purchase_multiplier: int,
|
purchase_multiplier: int,
|
||||||
|
now: datetime,
|
||||||
tracer: MultiEventTracer,
|
tracer: MultiEventTracer,
|
||||||
) -> Optional[Package]:
|
) -> Optional[Package]:
|
||||||
"""Allocates aircraft for a proposed mission and adds it to the ATO."""
|
"""Allocates aircraft for a proposed mission and adds it to the ATO."""
|
||||||
@ -221,6 +223,6 @@ class PackageFulfiller:
|
|||||||
|
|
||||||
if package.has_players and self.player_missions_asap:
|
if package.has_players and self.player_missions_asap:
|
||||||
package.auto_asap = True
|
package.auto_asap = True
|
||||||
package.set_tot_asap()
|
package.set_tot_asap(now)
|
||||||
|
|
||||||
return package
|
return package
|
||||||
|
|||||||
@ -104,6 +104,7 @@ class PackagePlanningTask(TheaterCommanderTask, Generic[MissionTargetT]):
|
|||||||
self.package = fulfiller.plan_mission(
|
self.package = fulfiller.plan_mission(
|
||||||
ProposedMission(self.target, self.flights),
|
ProposedMission(self.target, self.flights),
|
||||||
self.purchase_multiplier,
|
self.purchase_multiplier,
|
||||||
|
state.context.now,
|
||||||
state.context.tracer,
|
state.context.tracer,
|
||||||
)
|
)
|
||||||
return self.package is not None
|
return self.package is not None
|
||||||
|
|||||||
@ -54,6 +54,7 @@ https://en.wikipedia.org/wiki/Hierarchical_task_network
|
|||||||
"""
|
"""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from datetime import datetime
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
from game.ato.starttype import StartType
|
from game.ato.starttype import StartType
|
||||||
@ -77,8 +78,8 @@ class TheaterCommander(Planner[TheaterState, TheaterCommanderTask]):
|
|||||||
self.game = game
|
self.game = game
|
||||||
self.player = player
|
self.player = player
|
||||||
|
|
||||||
def plan_missions(self, tracer: MultiEventTracer) -> None:
|
def plan_missions(self, now: datetime, tracer: MultiEventTracer) -> None:
|
||||||
state = TheaterState.from_game(self.game, self.player, tracer)
|
state = TheaterState.from_game(self.game, self.player, now, tracer)
|
||||||
while True:
|
while True:
|
||||||
result = self.plan(state)
|
result = self.plan(state)
|
||||||
if result is None:
|
if result is None:
|
||||||
|
|||||||
@ -5,6 +5,7 @@ import itertools
|
|||||||
import math
|
import math
|
||||||
from collections.abc import Iterator
|
from collections.abc import Iterator
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
|
from datetime import datetime
|
||||||
from typing import Optional, TYPE_CHECKING, Union
|
from typing import Optional, TYPE_CHECKING, Union
|
||||||
|
|
||||||
from game.commander.battlepositions import BattlePositions
|
from game.commander.battlepositions import BattlePositions
|
||||||
@ -36,6 +37,7 @@ class PersistentContext:
|
|||||||
coalition: Coalition
|
coalition: Coalition
|
||||||
theater: ConflictTheater
|
theater: ConflictTheater
|
||||||
turn: int
|
turn: int
|
||||||
|
now: datetime
|
||||||
settings: Settings
|
settings: Settings
|
||||||
tracer: MultiEventTracer
|
tracer: MultiEventTracer
|
||||||
|
|
||||||
@ -137,14 +139,20 @@ class TheaterState(WorldState["TheaterState"]):
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_game(
|
def from_game(
|
||||||
cls, game: Game, player: bool, tracer: MultiEventTracer
|
cls, game: Game, player: bool, now: datetime, tracer: MultiEventTracer
|
||||||
) -> TheaterState:
|
) -> TheaterState:
|
||||||
coalition = game.coalition_for(player)
|
coalition = game.coalition_for(player)
|
||||||
finder = ObjectiveFinder(game, player)
|
finder = ObjectiveFinder(game, player)
|
||||||
ordered_capturable_points = finder.prioritized_unisolated_points()
|
ordered_capturable_points = finder.prioritized_unisolated_points()
|
||||||
|
|
||||||
context = PersistentContext(
|
context = PersistentContext(
|
||||||
game.db, coalition, game.theater, game.turn, game.settings, tracer
|
game.db,
|
||||||
|
coalition,
|
||||||
|
game.theater,
|
||||||
|
game.turn,
|
||||||
|
now,
|
||||||
|
game.settings,
|
||||||
|
tracer,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Plan enough rounds of CAP that the target has coverage over the expected
|
# Plan enough rounds of CAP that the target has coverage over the expected
|
||||||
|
|||||||
@ -2,6 +2,7 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import logging
|
import logging
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
|
from datetime import datetime
|
||||||
from typing import Optional, TYPE_CHECKING
|
from typing import Optional, TYPE_CHECKING
|
||||||
|
|
||||||
from game.theater import ControlPoint
|
from game.theater import ControlPoint
|
||||||
@ -52,7 +53,7 @@ class GroundUnitOrders:
|
|||||||
pending_units = 0
|
pending_units = 0
|
||||||
return pending_units
|
return pending_units
|
||||||
|
|
||||||
def process(self, game: Game) -> None:
|
def process(self, game: Game, now: datetime) -> None:
|
||||||
coalition = game.coalition_for(self.destination.captured)
|
coalition = game.coalition_for(self.destination.captured)
|
||||||
ground_unit_source = self.find_ground_unit_source(game)
|
ground_unit_source = self.find_ground_unit_source(game)
|
||||||
if ground_unit_source is None:
|
if ground_unit_source is None:
|
||||||
@ -95,15 +96,20 @@ class GroundUnitOrders:
|
|||||||
"still tried to transfer units to there"
|
"still tried to transfer units to there"
|
||||||
)
|
)
|
||||||
ground_unit_source.base.commission_units(units_needing_transfer)
|
ground_unit_source.base.commission_units(units_needing_transfer)
|
||||||
self.create_transfer(coalition, ground_unit_source, units_needing_transfer)
|
self.create_transfer(
|
||||||
|
coalition, ground_unit_source, units_needing_transfer, now
|
||||||
|
)
|
||||||
|
|
||||||
def create_transfer(
|
def create_transfer(
|
||||||
self,
|
self,
|
||||||
coalition: Coalition,
|
coalition: Coalition,
|
||||||
source: ControlPoint,
|
source: ControlPoint,
|
||||||
units: dict[GroundUnitType, int],
|
units: dict[GroundUnitType, int],
|
||||||
|
now: datetime,
|
||||||
) -> None:
|
) -> None:
|
||||||
coalition.transfers.new_transfer(TransferOrder(source, self.destination, units))
|
coalition.transfers.new_transfer(
|
||||||
|
TransferOrder(source, self.destination, units), now
|
||||||
|
)
|
||||||
|
|
||||||
def find_ground_unit_source(self, game: Game) -> Optional[ControlPoint]:
|
def find_ground_unit_source(self, game: Game) -> Optional[ControlPoint]:
|
||||||
# This is running *after* the turn counter has been incremented, so this is the
|
# This is running *after* the turn counter has been incremented, so this is the
|
||||||
|
|||||||
@ -94,7 +94,6 @@ class FlightGroupConfigurator:
|
|||||||
self.flight,
|
self.flight,
|
||||||
self.group,
|
self.group,
|
||||||
self.mission,
|
self.mission,
|
||||||
self.game.conditions.start_time,
|
|
||||||
self.time,
|
self.time,
|
||||||
self.game.settings,
|
self.game.settings,
|
||||||
self.mission_data,
|
self.mission_data,
|
||||||
|
|||||||
@ -22,8 +22,6 @@ class HoldPointBuilder(PydcsWaypointBuilder):
|
|||||||
return
|
return
|
||||||
push_time = self.flight.flight_plan.push_time
|
push_time = self.flight.flight_plan.push_time
|
||||||
self.waypoint.departure_time = push_time
|
self.waypoint.departure_time = push_time
|
||||||
loiter.stop_after_time(
|
loiter.stop_after_time(int((push_time - self.now).total_seconds()))
|
||||||
int((push_time - self.elapsed_mission_time).total_seconds())
|
|
||||||
)
|
|
||||||
waypoint.add_task(loiter)
|
waypoint.add_task(loiter)
|
||||||
waypoint.add_task(OptFormation.finger_four_close())
|
waypoint.add_task(OptFormation.finger_four_close())
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from datetime import timedelta
|
from datetime import datetime
|
||||||
from typing import Any, Iterable, Union
|
from typing import Any, Iterable, Union
|
||||||
|
|
||||||
from dcs import Mission
|
from dcs import Mission
|
||||||
@ -28,7 +28,7 @@ class PydcsWaypointBuilder:
|
|||||||
group: FlyingGroup[Any],
|
group: FlyingGroup[Any],
|
||||||
flight: Flight,
|
flight: Flight,
|
||||||
mission: Mission,
|
mission: Mission,
|
||||||
elapsed_mission_time: timedelta,
|
now: datetime,
|
||||||
mission_data: MissionData,
|
mission_data: MissionData,
|
||||||
unit_map: UnitMap,
|
unit_map: UnitMap,
|
||||||
) -> None:
|
) -> None:
|
||||||
@ -37,7 +37,7 @@ class PydcsWaypointBuilder:
|
|||||||
self.package = flight.package
|
self.package = flight.package
|
||||||
self.flight = flight
|
self.flight = flight
|
||||||
self.mission = mission
|
self.mission = mission
|
||||||
self.elapsed_mission_time = elapsed_mission_time
|
self.now = now
|
||||||
self.mission_data = mission_data
|
self.mission_data = mission_data
|
||||||
self.unit_map = unit_map
|
self.unit_map = unit_map
|
||||||
|
|
||||||
@ -68,10 +68,10 @@ class PydcsWaypointBuilder:
|
|||||||
def add_tasks(self, waypoint: MovingPoint) -> None:
|
def add_tasks(self, waypoint: MovingPoint) -> None:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def set_waypoint_tot(self, waypoint: MovingPoint, tot: timedelta) -> None:
|
def set_waypoint_tot(self, waypoint: MovingPoint, tot: datetime) -> None:
|
||||||
self.waypoint.tot = tot
|
self.waypoint.tot = tot
|
||||||
if not self._viggen_client_tot():
|
if not self._viggen_client_tot():
|
||||||
waypoint.ETA = int((tot - self.elapsed_mission_time).total_seconds())
|
waypoint.ETA = int((tot - self.now).total_seconds())
|
||||||
waypoint.ETA_locked = True
|
waypoint.ETA_locked = True
|
||||||
waypoint.speed_locked = False
|
waypoint.speed_locked = False
|
||||||
|
|
||||||
|
|||||||
@ -56,7 +56,7 @@ class RaceTrackBuilder(PydcsWaypointBuilder):
|
|||||||
|
|
||||||
racetrack = ControlledTask(orbit)
|
racetrack = ControlledTask(orbit)
|
||||||
self.set_waypoint_tot(waypoint, flight_plan.patrol_start_time)
|
self.set_waypoint_tot(waypoint, flight_plan.patrol_start_time)
|
||||||
loiter_duration = flight_plan.patrol_end_time - self.elapsed_mission_time
|
loiter_duration = flight_plan.patrol_end_time - self.now
|
||||||
racetrack.stop_after_time(int(loiter_duration.total_seconds()))
|
racetrack.stop_after_time(int(loiter_duration.total_seconds()))
|
||||||
waypoint.add_task(racetrack)
|
waypoint.add_task(racetrack)
|
||||||
|
|
||||||
|
|||||||
@ -25,13 +25,13 @@ from game.settings import Settings
|
|||||||
from game.unitmap import UnitMap
|
from game.unitmap import UnitMap
|
||||||
from game.utils import pairwise
|
from game.utils import pairwise
|
||||||
from .baiingress import BaiIngressBuilder
|
from .baiingress import BaiIngressBuilder
|
||||||
from .landingzone import LandingZoneBuilder
|
|
||||||
from .casingress import CasIngressBuilder
|
from .casingress import CasIngressBuilder
|
||||||
from .deadingress import DeadIngressBuilder
|
from .deadingress import DeadIngressBuilder
|
||||||
from .default import DefaultWaypointBuilder
|
from .default import DefaultWaypointBuilder
|
||||||
from .holdpoint import HoldPointBuilder
|
from .holdpoint import HoldPointBuilder
|
||||||
from .joinpoint import JoinPointBuilder
|
from .joinpoint import JoinPointBuilder
|
||||||
from .landingpoint import LandingPointBuilder
|
from .landingpoint import LandingPointBuilder
|
||||||
|
from .landingzone import LandingZoneBuilder
|
||||||
from .ocaaircraftingress import OcaAircraftIngressBuilder
|
from .ocaaircraftingress import OcaAircraftIngressBuilder
|
||||||
from .ocarunwayingress import OcaRunwayIngressBuilder
|
from .ocarunwayingress import OcaRunwayIngressBuilder
|
||||||
from .pydcswaypointbuilder import PydcsWaypointBuilder, TARGET_WAYPOINTS
|
from .pydcswaypointbuilder import PydcsWaypointBuilder, TARGET_WAYPOINTS
|
||||||
@ -50,7 +50,6 @@ class WaypointGenerator:
|
|||||||
flight: Flight,
|
flight: Flight,
|
||||||
group: FlyingGroup[Any],
|
group: FlyingGroup[Any],
|
||||||
mission: Mission,
|
mission: Mission,
|
||||||
turn_start_time: datetime,
|
|
||||||
time: datetime,
|
time: datetime,
|
||||||
settings: Settings,
|
settings: Settings,
|
||||||
mission_data: MissionData,
|
mission_data: MissionData,
|
||||||
@ -59,7 +58,6 @@ class WaypointGenerator:
|
|||||||
self.flight = flight
|
self.flight = flight
|
||||||
self.group = group
|
self.group = group
|
||||||
self.mission = mission
|
self.mission = mission
|
||||||
self.elapsed_mission_time = time - turn_start_time
|
|
||||||
self.time = time
|
self.time = time
|
||||||
self.settings = settings
|
self.settings = settings
|
||||||
self.mission_data = mission_data
|
self.mission_data = mission_data
|
||||||
@ -150,7 +148,7 @@ class WaypointGenerator:
|
|||||||
self.group,
|
self.group,
|
||||||
self.flight,
|
self.flight,
|
||||||
self.mission,
|
self.mission,
|
||||||
self.elapsed_mission_time,
|
self.time,
|
||||||
self.mission_data,
|
self.mission_data,
|
||||||
self.unit_map,
|
self.unit_map,
|
||||||
)
|
)
|
||||||
@ -182,12 +180,29 @@ class WaypointGenerator:
|
|||||||
a.min_fuel = min_fuel
|
a.min_fuel = min_fuel
|
||||||
|
|
||||||
def set_takeoff_time(self, waypoint: FlightWaypoint) -> timedelta:
|
def set_takeoff_time(self, waypoint: FlightWaypoint) -> timedelta:
|
||||||
|
force_delay = False
|
||||||
if isinstance(self.flight.state, WaitingForStart):
|
if isinstance(self.flight.state, WaitingForStart):
|
||||||
delay = self.flight.state.time_remaining(self.time)
|
delay = self.flight.state.time_remaining(self.time)
|
||||||
|
elif (
|
||||||
|
# The first two clauses capture the flight states that we want to adjust. We
|
||||||
|
# don't want to delay any flights that are already in flight or on the
|
||||||
|
# runway.
|
||||||
|
not self.flight.state.in_flight
|
||||||
|
and self.flight.state.spawn_type is not StartType.RUNWAY
|
||||||
|
and self.flight.departure.is_fleet
|
||||||
|
and not self.flight.client_count
|
||||||
|
):
|
||||||
|
# https://github.com/dcs-liberation/dcs_liberation/issues/1309
|
||||||
|
# Without a delay, AI aircraft will be spawned on the sixpack, which other
|
||||||
|
# AI planes of course want to taxi through, deadlocking the carrier deck.
|
||||||
|
# Delaying AI carrier deck spawns by one second for some reason causes DCS
|
||||||
|
# to spawn those aircraft elsewhere, avoiding the traffic jam.
|
||||||
|
delay = timedelta(seconds=1)
|
||||||
|
force_delay = True
|
||||||
else:
|
else:
|
||||||
delay = timedelta()
|
delay = timedelta()
|
||||||
|
|
||||||
if self.should_delay_flight():
|
if force_delay or self.should_delay_flight():
|
||||||
if self.should_activate_late():
|
if self.should_activate_late():
|
||||||
# Late activation causes the aircraft to not be spawned
|
# Late activation causes the aircraft to not be spawned
|
||||||
# until triggered.
|
# until triggered.
|
||||||
|
|||||||
@ -5,7 +5,6 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import os
|
import os
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from datetime import timedelta
|
|
||||||
from typing import Dict, List, TYPE_CHECKING
|
from typing import Dict, List, TYPE_CHECKING
|
||||||
|
|
||||||
from dcs.mission import Mission
|
from dcs.mission import Mission
|
||||||
@ -127,11 +126,9 @@ class MissionInfoGenerator:
|
|||||||
|
|
||||||
def format_waypoint_time(waypoint: FlightWaypoint, depart_prefix: str) -> str:
|
def format_waypoint_time(waypoint: FlightWaypoint, depart_prefix: str) -> str:
|
||||||
if waypoint.tot is not None:
|
if waypoint.tot is not None:
|
||||||
time = timedelta(seconds=int(waypoint.tot.total_seconds()))
|
return f"{waypoint.tot.time()} "
|
||||||
return f"T+{time} "
|
|
||||||
elif waypoint.departure_time is not None:
|
elif waypoint.departure_time is not None:
|
||||||
time = timedelta(seconds=int(waypoint.departure_time.total_seconds()))
|
return f"{depart_prefix} {waypoint.departure_time.time()} "
|
||||||
return f"{depart_prefix} T+{time} "
|
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -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:
|
if time is None:
|
||||||
return ""
|
return ""
|
||||||
local_time = self.start_time + time
|
return f"{time.strftime('%H:%M:%S')}{'Z' if time.tzinfo is not None else ''}"
|
||||||
return f"{local_time.strftime('%H:%M:%S')}{'Z' if local_time.tzinfo is not None else ''}"
|
|
||||||
|
|
||||||
def _format_alt(self, alt: Distance) -> str:
|
def _format_alt(self, alt: Distance) -> str:
|
||||||
return f"{self.units.distance_short(alt):.0f}"
|
return f"{self.units.distance_short(alt):.0f}"
|
||||||
@ -583,11 +583,11 @@ class SupportPage(KneeboardPage):
|
|||||||
)
|
)
|
||||||
return f"{channel_name}\n{frequency}"
|
return f"{channel_name}\n{frequency}"
|
||||||
|
|
||||||
def _format_time(self, time: Optional[datetime.timedelta]) -> str:
|
@staticmethod
|
||||||
|
def _format_time(time: datetime.datetime | None) -> str:
|
||||||
if time is None:
|
if time is None:
|
||||||
return ""
|
return ""
|
||||||
local_time = self.start_time + time
|
return f"{time.strftime('%H:%M:%S')}{'Z' if time.tzinfo is not None else ''}"
|
||||||
return f"{local_time.strftime('%H:%M:%S')}{'Z' if local_time.tzinfo is not None else ''}"
|
|
||||||
|
|
||||||
|
|
||||||
class SeadTaskPage(KneeboardPage):
|
class SeadTaskPage(KneeboardPage):
|
||||||
|
|||||||
@ -1,11 +1,11 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
from datetime import timedelta
|
from datetime import datetime
|
||||||
from typing import Optional, TYPE_CHECKING
|
from typing import Optional, TYPE_CHECKING
|
||||||
|
|
||||||
from game.dcs.aircrafttype import AircraftType
|
from game.dcs.aircrafttype import AircraftType
|
||||||
from game.missiongenerator.aircraft.flightdata import FlightData
|
from game.missiongenerator.aircraft.flightdata import FlightData
|
||||||
|
|
||||||
from game.runways import RunwayData
|
from game.runways import RunwayData
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
@ -31,8 +31,8 @@ class AwacsInfo(GroupInfo):
|
|||||||
"""AWACS information for the kneeboard."""
|
"""AWACS information for the kneeboard."""
|
||||||
|
|
||||||
depature_location: Optional[str]
|
depature_location: Optional[str]
|
||||||
start_time: Optional[timedelta]
|
start_time: datetime | None
|
||||||
end_time: Optional[timedelta]
|
end_time: datetime | None
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
@ -41,8 +41,8 @@ class TankerInfo(GroupInfo):
|
|||||||
|
|
||||||
variant: str
|
variant: str
|
||||||
tacan: TacanChannel
|
tacan: TacanChannel
|
||||||
start_time: Optional[timedelta]
|
start_time: datetime | None
|
||||||
end_time: Optional[timedelta]
|
end_time: datetime | None
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
|
|||||||
@ -1,7 +1,5 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from datetime import timedelta
|
|
||||||
|
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
|
|
||||||
from game.ato import Flight, FlightWaypoint
|
from game.ato import Flight, FlightWaypoint
|
||||||
@ -21,7 +19,7 @@ def timing_info(flight: Flight, waypoint_idx: int) -> str:
|
|||||||
time = flight.flight_plan.depart_time_for_waypoint(waypoint)
|
time = flight.flight_plan.depart_time_for_waypoint(waypoint)
|
||||||
if time is None:
|
if time is None:
|
||||||
return ""
|
return ""
|
||||||
return f"{prefix} T+{timedelta(seconds=int(time.total_seconds()))}"
|
return f"{prefix} {time}"
|
||||||
|
|
||||||
|
|
||||||
class FlightWaypointJs(BaseModel):
|
class FlightWaypointJs(BaseModel):
|
||||||
|
|||||||
@ -74,11 +74,11 @@ class AircraftSimulation:
|
|||||||
now = self.game.conditions.start_time
|
now = self.game.conditions.start_time
|
||||||
for flight in self.iter_flights():
|
for flight in self.iter_flights():
|
||||||
start_time = flight.flight_plan.startup_time()
|
start_time = flight.flight_plan.startup_time()
|
||||||
if start_time <= timedelta():
|
if start_time <= now:
|
||||||
self.set_active_flight_state(flight, now)
|
self.set_active_flight_state(flight, now)
|
||||||
else:
|
else:
|
||||||
flight.set_state(
|
flight.set_state(
|
||||||
WaitingForStart(flight, self.game.settings, now + start_time)
|
WaitingForStart(flight, self.game.settings, start_time)
|
||||||
)
|
)
|
||||||
|
|
||||||
def set_active_flight_state(self, flight: Flight, now: datetime) -> None:
|
def set_active_flight_state(self, flight: Flight, now: datetime) -> None:
|
||||||
|
|||||||
@ -4,6 +4,7 @@ import logging
|
|||||||
import random
|
import random
|
||||||
from collections.abc import Iterable
|
from collections.abc import Iterable
|
||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
|
from datetime import datetime
|
||||||
from typing import Optional, Sequence, TYPE_CHECKING
|
from typing import Optional, Sequence, TYPE_CHECKING
|
||||||
|
|
||||||
from faker import Faker
|
from faker import Faker
|
||||||
@ -334,7 +335,7 @@ class Squadron:
|
|||||||
def arrival(self) -> ControlPoint:
|
def arrival(self) -> ControlPoint:
|
||||||
return self.location if self.destination is None else self.destination
|
return self.location if self.destination is None else self.destination
|
||||||
|
|
||||||
def plan_relocation(self, destination: ControlPoint) -> None:
|
def plan_relocation(self, destination: ControlPoint, now: datetime) -> None:
|
||||||
if destination == self.location:
|
if destination == self.location:
|
||||||
logging.warning(
|
logging.warning(
|
||||||
f"Attempted to plan relocation of {self} to current location "
|
f"Attempted to plan relocation of {self} to current location "
|
||||||
@ -353,7 +354,7 @@ class Squadron:
|
|||||||
if not destination.can_operate(self.aircraft):
|
if not destination.can_operate(self.aircraft):
|
||||||
raise RuntimeError(f"{self} cannot operate at {destination}.")
|
raise RuntimeError(f"{self} cannot operate at {destination}.")
|
||||||
self.destination = destination
|
self.destination = destination
|
||||||
self.replan_ferry_flights()
|
self.replan_ferry_flights(now)
|
||||||
|
|
||||||
def cancel_relocation(self) -> None:
|
def cancel_relocation(self) -> None:
|
||||||
if self.destination is None:
|
if self.destination is None:
|
||||||
@ -368,9 +369,9 @@ class Squadron:
|
|||||||
self.destination = None
|
self.destination = None
|
||||||
self.cancel_ferry_flights()
|
self.cancel_ferry_flights()
|
||||||
|
|
||||||
def replan_ferry_flights(self) -> None:
|
def replan_ferry_flights(self, now: datetime) -> None:
|
||||||
self.cancel_ferry_flights()
|
self.cancel_ferry_flights()
|
||||||
self.plan_ferry_flights()
|
self.plan_ferry_flights(now)
|
||||||
|
|
||||||
def cancel_ferry_flights(self) -> None:
|
def cancel_ferry_flights(self) -> None:
|
||||||
for package in self.coalition.ato.packages:
|
for package in self.coalition.ato.packages:
|
||||||
@ -381,7 +382,7 @@ class Squadron:
|
|||||||
if not package.flights:
|
if not package.flights:
|
||||||
self.coalition.ato.remove_package(package)
|
self.coalition.ato.remove_package(package)
|
||||||
|
|
||||||
def plan_ferry_flights(self) -> None:
|
def plan_ferry_flights(self, now: datetime) -> None:
|
||||||
if self.destination is None:
|
if self.destination is None:
|
||||||
raise RuntimeError(
|
raise RuntimeError(
|
||||||
f"Cannot plan ferry flights for {self} because there is no destination."
|
f"Cannot plan ferry flights for {self} because there is no destination."
|
||||||
@ -395,7 +396,7 @@ class Squadron:
|
|||||||
size = min(remaining, self.aircraft.max_group_size)
|
size = min(remaining, self.aircraft.max_group_size)
|
||||||
self.plan_ferry_flight(package, size)
|
self.plan_ferry_flight(package, size)
|
||||||
remaining -= size
|
remaining -= size
|
||||||
package.set_tot_asap()
|
package.set_tot_asap(now)
|
||||||
self.coalition.ato.add_package(package)
|
self.coalition.ato.add_package(package)
|
||||||
|
|
||||||
def plan_ferry_flight(self, package: Package, size: int) -> None:
|
def plan_ferry_flight(self, package: Package, size: int) -> None:
|
||||||
|
|||||||
@ -902,7 +902,11 @@ class ControlPoint(MissionTarget, SidcDescribable, ABC):
|
|||||||
self.runway_status.begin_repair()
|
self.runway_status.begin_repair()
|
||||||
|
|
||||||
def process_turn(self, game: Game) -> None:
|
def process_turn(self, game: Game) -> None:
|
||||||
self.ground_unit_orders.process(game)
|
# We're running at the end of the turn, so the time right now is irrelevant, and
|
||||||
|
# we don't know what time the next turn will start yet. It doesn't actually
|
||||||
|
# matter though, because the first thing the start of turn action will do is
|
||||||
|
# clear the ATO and replan the airlifts with the correct time.
|
||||||
|
self.ground_unit_orders.process(game, game.conditions.start_time)
|
||||||
|
|
||||||
runway_status = self.runway_status
|
runway_status = self.runway_status
|
||||||
if runway_status is not None:
|
if runway_status is not None:
|
||||||
|
|||||||
@ -35,6 +35,7 @@ import logging
|
|||||||
import math
|
import math
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
|
from datetime import datetime
|
||||||
from functools import singledispatchmethod
|
from functools import singledispatchmethod
|
||||||
from typing import Generic, Iterator, List, Optional, Sequence, TYPE_CHECKING, TypeVar
|
from typing import Generic, Iterator, List, Optional, Sequence, TYPE_CHECKING, TypeVar
|
||||||
|
|
||||||
@ -299,7 +300,7 @@ class AirliftPlanner:
|
|||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def create_package_for_airlift(self) -> None:
|
def create_package_for_airlift(self, now: datetime) -> None:
|
||||||
distance_cache = ObjectiveDistanceCache.get_closest_airfields(
|
distance_cache = ObjectiveDistanceCache.get_closest_airfields(
|
||||||
self.transfer.position
|
self.transfer.position
|
||||||
)
|
)
|
||||||
@ -318,7 +319,7 @@ class AirliftPlanner:
|
|||||||
):
|
):
|
||||||
self.create_airlift_flight(squadron)
|
self.create_airlift_flight(squadron)
|
||||||
if self.package.flights:
|
if self.package.flights:
|
||||||
self.package.set_tot_asap()
|
self.package.set_tot_asap(now)
|
||||||
self.game.ato_for(self.for_player).add_package(self.package)
|
self.game.ato_for(self.for_player).add_package(self.package)
|
||||||
|
|
||||||
def create_airlift_flight(self, squadron: Squadron) -> int:
|
def create_airlift_flight(self, squadron: Squadron) -> int:
|
||||||
@ -580,7 +581,7 @@ class PendingTransfers:
|
|||||||
def network_for(self, control_point: ControlPoint) -> TransitNetwork:
|
def network_for(self, control_point: ControlPoint) -> TransitNetwork:
|
||||||
return self.game.transit_network_for(control_point.captured)
|
return self.game.transit_network_for(control_point.captured)
|
||||||
|
|
||||||
def arrange_transport(self, transfer: TransferOrder) -> None:
|
def arrange_transport(self, transfer: TransferOrder, now: datetime) -> None:
|
||||||
network = self.network_for(transfer.position)
|
network = self.network_for(transfer.position)
|
||||||
path = network.shortest_path_between(transfer.position, transfer.destination)
|
path = network.shortest_path_between(transfer.position, transfer.destination)
|
||||||
next_stop = path[0]
|
next_stop = path[0]
|
||||||
@ -595,12 +596,12 @@ class PendingTransfers:
|
|||||||
== TransitConnection.Shipping
|
== TransitConnection.Shipping
|
||||||
):
|
):
|
||||||
return self.cargo_ships.add(transfer, next_stop)
|
return self.cargo_ships.add(transfer, next_stop)
|
||||||
AirliftPlanner(self.game, transfer, next_stop).create_package_for_airlift()
|
AirliftPlanner(self.game, transfer, next_stop).create_package_for_airlift(now)
|
||||||
|
|
||||||
def new_transfer(self, transfer: TransferOrder) -> None:
|
def new_transfer(self, transfer: TransferOrder, now: datetime) -> None:
|
||||||
transfer.origin.base.commit_losses(transfer.units)
|
transfer.origin.base.commit_losses(transfer.units)
|
||||||
self.pending_transfers.append(transfer)
|
self.pending_transfers.append(transfer)
|
||||||
self.arrange_transport(transfer)
|
self.arrange_transport(transfer, now)
|
||||||
|
|
||||||
def split_transfer(self, transfer: TransferOrder, size: int) -> TransferOrder:
|
def split_transfer(self, transfer: TransferOrder, size: int) -> TransferOrder:
|
||||||
"""Creates a smaller transfer that is a subset of the original."""
|
"""Creates a smaller transfer that is a subset of the original."""
|
||||||
@ -672,7 +673,7 @@ class PendingTransfers:
|
|||||||
self.convoys.disband_all()
|
self.convoys.disband_all()
|
||||||
self.cargo_ships.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
|
Plan transports for all pending and completable transfers which don't have a
|
||||||
transport assigned already. This calculates the shortest path between current
|
transport assigned already. This calculates the shortest path between current
|
||||||
@ -682,7 +683,7 @@ class PendingTransfers:
|
|||||||
self.disband_uncompletable_transfers()
|
self.disband_uncompletable_transfers()
|
||||||
for transfer in self.pending_transfers:
|
for transfer in self.pending_transfers:
|
||||||
if transfer.transport is None:
|
if transfer.transport is None:
|
||||||
self.arrange_transport(transfer)
|
self.arrange_transport(transfer, now)
|
||||||
|
|
||||||
def disband_uncompletable_transfers(self) -> None:
|
def disband_uncompletable_transfers(self) -> None:
|
||||||
"""
|
"""
|
||||||
|
|||||||
@ -138,11 +138,9 @@ class PackageModel(QAbstractListModel):
|
|||||||
@staticmethod
|
@staticmethod
|
||||||
def text_for_flight(flight: Flight) -> str:
|
def text_for_flight(flight: Flight) -> str:
|
||||||
"""Returns the text that should be displayed for the flight."""
|
"""Returns the text that should be displayed for the flight."""
|
||||||
delay = datetime.timedelta(
|
|
||||||
seconds=int(flight.flight_plan.startup_time().total_seconds())
|
|
||||||
)
|
|
||||||
origin = flight.from_cp.name
|
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
|
@staticmethod
|
||||||
def icon_for_flight(flight: Flight) -> Optional[QIcon]:
|
def icon_for_flight(flight: Flight) -> Optional[QIcon]:
|
||||||
@ -184,7 +182,7 @@ class PackageModel(QAbstractListModel):
|
|||||||
"""Returns the flight located at the given index."""
|
"""Returns the flight located at the given index."""
|
||||||
return self.package.flights[index.row()]
|
return self.package.flights[index.row()]
|
||||||
|
|
||||||
def set_tot(self, tot: datetime.timedelta) -> None:
|
def set_tot(self, tot: datetime.datetime) -> None:
|
||||||
self.package.time_over_target = tot
|
self.package.time_over_target = tot
|
||||||
self.update_tot()
|
self.update_tot()
|
||||||
|
|
||||||
@ -194,7 +192,9 @@ class PackageModel(QAbstractListModel):
|
|||||||
|
|
||||||
def update_tot(self) -> None:
|
def update_tot(self) -> None:
|
||||||
if self.package.auto_asap:
|
if self.package.auto_asap:
|
||||||
self.package.set_tot_asap()
|
self.package.set_tot_asap(
|
||||||
|
self.game_model.sim_controller.current_time_in_sim
|
||||||
|
)
|
||||||
self.tot_changed.emit()
|
self.tot_changed.emit()
|
||||||
# For some reason this is needed to make the UI update quickly.
|
# For some reason this is needed to make the UI update quickly.
|
||||||
self.layoutChanged.emit()
|
self.layoutChanged.emit()
|
||||||
@ -381,11 +381,11 @@ class TransferModel(QAbstractListModel):
|
|||||||
"""Returns the icon that should be displayed for the transfer."""
|
"""Returns the icon that should be displayed for the transfer."""
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def new_transfer(self, transfer: TransferOrder) -> None:
|
def new_transfer(self, transfer: TransferOrder, now: datetime) -> None:
|
||||||
"""Updates the game with the new unit transfer."""
|
"""Updates the game with the new unit transfer."""
|
||||||
self.beginInsertRows(QModelIndex(), self.rowCount(), self.rowCount())
|
self.beginInsertRows(QModelIndex(), self.rowCount(), self.rowCount())
|
||||||
# TODO: Needs to regenerate base inventory tab.
|
# TODO: Needs to regenerate base inventory tab.
|
||||||
self.transfers.new_transfer(transfer)
|
self.transfers.new_transfer(transfer, now)
|
||||||
self.endInsertRows()
|
self.endInsertRows()
|
||||||
|
|
||||||
def cancel_transfer_at_index(self, index: QModelIndex) -> None:
|
def cancel_transfer_at_index(self, index: QModelIndex) -> None:
|
||||||
|
|||||||
@ -34,11 +34,18 @@ class SimController(QObject):
|
|||||||
return self.game_loop.completed
|
return self.game_loop.completed
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def current_time_in_sim(self) -> Optional[datetime]:
|
def current_time_in_sim_if_game_loaded(self) -> datetime | None:
|
||||||
if self.game_loop is None:
|
if self.game_loop is None:
|
||||||
return None
|
return None
|
||||||
return self.game_loop.current_time_in_sim
|
return self.game_loop.current_time_in_sim
|
||||||
|
|
||||||
|
@property
|
||||||
|
def current_time_in_sim(self) -> datetime:
|
||||||
|
time = self.current_time_in_sim_if_game_loaded
|
||||||
|
if time is None:
|
||||||
|
raise RuntimeError("No game is loaded")
|
||||||
|
return time
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def elapsed_time(self) -> timedelta:
|
def elapsed_time(self) -> timedelta:
|
||||||
if self.game_loop is None:
|
if self.game_loop is None:
|
||||||
|
|||||||
@ -58,7 +58,7 @@ class QTimeTurnWidget(QGroupBox):
|
|||||||
sim_controller.sim_update.connect(self.on_sim_update)
|
sim_controller.sim_update.connect(self.on_sim_update)
|
||||||
|
|
||||||
def on_sim_update(self, _events: GameUpdateEvents) -> None:
|
def on_sim_update(self, _events: GameUpdateEvents) -> None:
|
||||||
time = self.sim_controller.current_time_in_sim
|
time = self.sim_controller.current_time_in_sim_if_game_loaded
|
||||||
if time is None:
|
if time is None:
|
||||||
self.date_display.setText("")
|
self.date_display.setText("")
|
||||||
self.time_display.setText("")
|
self.time_display.setText("")
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
from datetime import datetime
|
||||||
from typing import List, Optional
|
from typing import List, Optional
|
||||||
|
|
||||||
from PySide6.QtWidgets import (
|
from PySide6.QtWidgets import (
|
||||||
@ -155,13 +156,14 @@ class QTopPanel(QFrame):
|
|||||||
GameUpdateSignal.get_instance().updateGame(self.game)
|
GameUpdateSignal.get_instance().updateGame(self.game)
|
||||||
self.proceedButton.setEnabled(True)
|
self.proceedButton.setEnabled(True)
|
||||||
|
|
||||||
def negative_start_packages(self) -> List[Package]:
|
def negative_start_packages(self, now: datetime) -> List[Package]:
|
||||||
packages = []
|
packages = []
|
||||||
for package in self.game_model.ato_model.ato.packages:
|
for package in self.game_model.ato_model.ato.packages:
|
||||||
if not package.flights:
|
if not package.flights:
|
||||||
continue
|
continue
|
||||||
for flight in package.flights:
|
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)
|
packages.append(package)
|
||||||
break
|
break
|
||||||
return packages
|
return packages
|
||||||
@ -277,7 +279,9 @@ class QTopPanel(QFrame):
|
|||||||
if self.check_no_missing_pilots():
|
if self.check_no_missing_pilots():
|
||||||
return
|
return
|
||||||
|
|
||||||
negative_starts = self.negative_start_packages()
|
negative_starts = self.negative_start_packages(
|
||||||
|
self.sim_controller.current_time_in_sim
|
||||||
|
)
|
||||||
if negative_starts:
|
if negative_starts:
|
||||||
if not self.confirm_negative_start_time(negative_starts):
|
if not self.confirm_negative_start_time(negative_starts):
|
||||||
return
|
return
|
||||||
|
|||||||
@ -1,6 +1,5 @@
|
|||||||
"""Widgets for displaying air tasking orders."""
|
"""Widgets for displaying air tasking orders."""
|
||||||
import logging
|
import logging
|
||||||
from datetime import timedelta
|
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
from PySide6.QtCore import (
|
from PySide6.QtCore import (
|
||||||
@ -253,16 +252,7 @@ class PackageDelegate(TwoColumnRowDelegate):
|
|||||||
clients = self.num_clients(index)
|
clients = self.num_clients(index)
|
||||||
return f"Player Slots: {clients}" if clients else ""
|
return f"Player Slots: {clients}" if clients else ""
|
||||||
elif (row, column) == (1, 0):
|
elif (row, column) == (1, 0):
|
||||||
tot_delay = (
|
return f"TOT at {package.time_over_target:%H:%M:%S}"
|
||||||
package.time_over_target - self.game_model.sim_controller.elapsed_time
|
|
||||||
)
|
|
||||||
if tot_delay >= timedelta():
|
|
||||||
return f"TOT in {tot_delay}"
|
|
||||||
game = self.game_model.game
|
|
||||||
if game is None:
|
|
||||||
raise RuntimeError("Package TOT has elapsed but no game is loaded")
|
|
||||||
tot_time = game.conditions.start_time + package.time_over_target
|
|
||||||
return f"TOT passed at {tot_time:%H:%M:%S}"
|
|
||||||
elif (row, column) == (1, 1):
|
elif (row, column) == (1, 1):
|
||||||
unassigned_pilots = self.missing_pilots(index)
|
unassigned_pilots = self.missing_pilots(index)
|
||||||
return f"Missing pilots: {unassigned_pilots}" if unassigned_pilots else ""
|
return f"Missing pilots: {unassigned_pilots}" if unassigned_pilots else ""
|
||||||
|
|||||||
@ -1,27 +1,28 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from typing import Optional, Iterator
|
from typing import Iterator, Optional
|
||||||
|
|
||||||
from PySide6.QtCore import QItemSelectionModel, QModelIndex, QSize
|
from PySide6.QtCore import QItemSelectionModel, QModelIndex, QSize
|
||||||
from PySide6.QtWidgets import (
|
from PySide6.QtWidgets import (
|
||||||
QAbstractItemView,
|
QAbstractItemView,
|
||||||
QCheckBox,
|
QCheckBox,
|
||||||
QDialog,
|
QDialog,
|
||||||
|
QHBoxLayout,
|
||||||
QListView,
|
QListView,
|
||||||
QVBoxLayout,
|
|
||||||
QTabWidget,
|
QTabWidget,
|
||||||
QTableWidget,
|
QTableWidget,
|
||||||
QTableWidgetItem,
|
QTableWidgetItem,
|
||||||
|
QVBoxLayout,
|
||||||
QWidget,
|
QWidget,
|
||||||
QHBoxLayout,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
from game.ato.flight import Flight
|
||||||
from game.squadrons import Squadron
|
from game.squadrons import Squadron
|
||||||
from game.theater import ConflictTheater
|
from game.theater import ConflictTheater
|
||||||
from game.ato.flight import Flight
|
|
||||||
from qt_ui.delegates import TwoColumnRowDelegate
|
from qt_ui.delegates import TwoColumnRowDelegate
|
||||||
from qt_ui.models import GameModel, AirWingModel, SquadronModel, AtoModel
|
from qt_ui.models import AirWingModel, AtoModel, GameModel, SquadronModel
|
||||||
|
from qt_ui.simcontroller import SimController
|
||||||
from qt_ui.windows.SquadronDialog import SquadronDialog
|
from qt_ui.windows.SquadronDialog import SquadronDialog
|
||||||
|
|
||||||
|
|
||||||
@ -63,11 +64,13 @@ class SquadronList(QListView):
|
|||||||
ato_model: AtoModel,
|
ato_model: AtoModel,
|
||||||
air_wing_model: AirWingModel,
|
air_wing_model: AirWingModel,
|
||||||
theater: ConflictTheater,
|
theater: ConflictTheater,
|
||||||
|
sim_controller: SimController,
|
||||||
) -> None:
|
) -> None:
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.ato_model = ato_model
|
self.ato_model = ato_model
|
||||||
self.air_wing_model = air_wing_model
|
self.air_wing_model = air_wing_model
|
||||||
self.theater = theater
|
self.theater = theater
|
||||||
|
self.sim_controller = sim_controller
|
||||||
self.dialog: Optional[SquadronDialog] = None
|
self.dialog: Optional[SquadronDialog] = None
|
||||||
|
|
||||||
self.setIconSize(QSize(91, 24))
|
self.setIconSize(QSize(91, 24))
|
||||||
@ -88,6 +91,7 @@ class SquadronList(QListView):
|
|||||||
self.ato_model,
|
self.ato_model,
|
||||||
SquadronModel(self.air_wing_model.squadron_at_index(index)),
|
SquadronModel(self.air_wing_model.squadron_at_index(index)),
|
||||||
self.theater,
|
self.theater,
|
||||||
|
self.sim_controller,
|
||||||
self,
|
self,
|
||||||
)
|
)
|
||||||
self.dialog.show()
|
self.dialog.show()
|
||||||
@ -229,6 +233,7 @@ class AirWingTabs(QTabWidget):
|
|||||||
game_model.ato_model,
|
game_model.ato_model,
|
||||||
game_model.blue_air_wing_model,
|
game_model.blue_air_wing_model,
|
||||||
game_model.game.theater,
|
game_model.game.theater,
|
||||||
|
game_model.sim_controller,
|
||||||
),
|
),
|
||||||
"Squadrons",
|
"Squadrons",
|
||||||
)
|
)
|
||||||
|
|||||||
@ -20,6 +20,7 @@ from game.theater import ConflictTheater, ControlPoint
|
|||||||
from qt_ui.delegates import TwoColumnRowDelegate
|
from qt_ui.delegates import TwoColumnRowDelegate
|
||||||
from qt_ui.errorreporter import report_errors
|
from qt_ui.errorreporter import report_errors
|
||||||
from qt_ui.models import AtoModel, SquadronModel
|
from qt_ui.models import AtoModel, SquadronModel
|
||||||
|
from qt_ui.simcontroller import SimController
|
||||||
|
|
||||||
|
|
||||||
class PilotDelegate(TwoColumnRowDelegate):
|
class PilotDelegate(TwoColumnRowDelegate):
|
||||||
@ -134,11 +135,13 @@ class SquadronDialog(QDialog):
|
|||||||
ato_model: AtoModel,
|
ato_model: AtoModel,
|
||||||
squadron_model: SquadronModel,
|
squadron_model: SquadronModel,
|
||||||
theater: ConflictTheater,
|
theater: ConflictTheater,
|
||||||
|
sim_controller: SimController,
|
||||||
parent,
|
parent,
|
||||||
) -> None:
|
) -> None:
|
||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
self.ato_model = ato_model
|
self.ato_model = ato_model
|
||||||
self.squadron_model = squadron_model
|
self.squadron_model = squadron_model
|
||||||
|
self.sim_controller = sim_controller
|
||||||
|
|
||||||
self.setMinimumSize(1000, 440)
|
self.setMinimumSize(1000, 440)
|
||||||
self.setWindowTitle(str(squadron_model.squadron))
|
self.setWindowTitle(str(squadron_model.squadron))
|
||||||
@ -194,7 +197,9 @@ class SquadronDialog(QDialog):
|
|||||||
if destination is None:
|
if destination is None:
|
||||||
self.squadron.cancel_relocation()
|
self.squadron.cancel_relocation()
|
||||||
else:
|
else:
|
||||||
self.squadron.plan_relocation(destination)
|
self.squadron.plan_relocation(
|
||||||
|
destination, self.sim_controller.current_time_in_sim
|
||||||
|
)
|
||||||
self.ato_model.replace_from_game(player=True)
|
self.ato_model.replace_from_game(player=True)
|
||||||
|
|
||||||
def check_disabled_button_states(
|
def check_disabled_button_states(
|
||||||
|
|||||||
@ -303,7 +303,9 @@ class NewUnitTransferDialog(QDialog):
|
|||||||
units=transfers,
|
units=transfers,
|
||||||
request_airflift=self.dest_panel.request_airlift,
|
request_airflift=self.dest_panel.request_airlift,
|
||||||
)
|
)
|
||||||
self.game_model.transfer_model.new_transfer(transfer)
|
self.game_model.transfer_model.new_transfer(
|
||||||
|
transfer, self.game_model.sim_controller.current_time_in_sim
|
||||||
|
)
|
||||||
self.close()
|
self.close()
|
||||||
|
|
||||||
def on_transfer_quantity_changed(self) -> None:
|
def on_transfer_quantity_changed(self) -> None:
|
||||||
|
|||||||
@ -16,4 +16,4 @@ class QFlightItem(QStandardItem):
|
|||||||
icon = QIcon((AIRCRAFT_ICONS[self.flight.unit_type.dcs_id]))
|
icon = QIcon((AIRCRAFT_ICONS[self.flight.unit_type.dcs_id]))
|
||||||
self.setIcon(icon)
|
self.setIcon(icon)
|
||||||
self.setEditable(False)
|
self.setEditable(False)
|
||||||
self.setText(f"{flight} in {flight.flight_plan.startup_time()}")
|
self.setText(f"{flight} at {flight.flight_plan.startup_time()}")
|
||||||
|
|||||||
@ -1,6 +1,5 @@
|
|||||||
"""Dialogs for creating and editing ATO packages."""
|
"""Dialogs for creating and editing ATO packages."""
|
||||||
import logging
|
import logging
|
||||||
from datetime import timedelta
|
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
from PySide6.QtCore import QItemSelection, QTime, Qt, Signal
|
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 = QTimeEdit(self.tot_qtime())
|
||||||
self.tot_spinner.setMinimumTime(QTime(0, 0))
|
self.tot_spinner.setMinimumTime(QTime(0, 0))
|
||||||
self.tot_spinner.setDisplayFormat("T+hh:mm:ss")
|
self.tot_spinner.setDisplayFormat("hh:mm:ss")
|
||||||
self.tot_spinner.timeChanged.connect(self.save_tot)
|
self.tot_spinner.timeChanged.connect(self.save_tot)
|
||||||
self.tot_spinner.setToolTip("Package TOT relative to mission TOT")
|
self.tot_spinner.setToolTip("Package TOT relative to mission TOT")
|
||||||
self.tot_spinner.setEnabled(not self.package_model.package.auto_asap)
|
self.tot_spinner.setEnabled(not self.package_model.package.auto_asap)
|
||||||
@ -133,11 +132,8 @@ class QPackageDialog(QDialog):
|
|||||||
return self.game_model.game
|
return self.game_model.game
|
||||||
|
|
||||||
def tot_qtime(self) -> QTime:
|
def tot_qtime(self) -> QTime:
|
||||||
delay = int(self.package_model.package.time_over_target.total_seconds())
|
tot = self.package_model.package.time_over_target
|
||||||
hours = delay // 3600
|
return QTime(tot.hour, tot.minute, tot.second)
|
||||||
minutes = delay // 60 % 60
|
|
||||||
seconds = delay % 60
|
|
||||||
return QTime(hours, minutes, seconds)
|
|
||||||
|
|
||||||
def on_cancel(self) -> None:
|
def on_cancel(self) -> None:
|
||||||
pass
|
pass
|
||||||
@ -151,9 +147,13 @@ class QPackageDialog(QDialog):
|
|||||||
self.save_tot()
|
self.save_tot()
|
||||||
|
|
||||||
def save_tot(self) -> None:
|
def save_tot(self) -> None:
|
||||||
|
# TODO: This is going to break horribly around midnight.
|
||||||
time = self.tot_spinner.time()
|
time = self.tot_spinner.time()
|
||||||
seconds = time.hour() * 3600 + time.minute() * 60 + time.second()
|
self.package_model.set_tot(
|
||||||
self.package_model.set_tot(timedelta(seconds=seconds))
|
self.package_model.package.time_over_target.replace(
|
||||||
|
hour=time.hour(), minute=time.minute(), second=time.second()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
def set_asap(self, checked: bool) -> None:
|
def set_asap(self, checked: bool) -> None:
|
||||||
self.package_model.set_asap(checked)
|
self.package_model.set_asap(checked)
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
from datetime import timedelta
|
from datetime import datetime
|
||||||
|
|
||||||
from PySide6.QtCore import QItemSelectionModel, QSize
|
from PySide6.QtCore import QItemSelectionModel, QSize
|
||||||
from PySide6.QtGui import QStandardItemModel
|
from PySide6.QtGui import QStandardItemModel
|
||||||
@ -50,5 +50,5 @@ class QPlannedFlightsView(QListView):
|
|||||||
self.setup_content()
|
self.setup_content()
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def mission_start_for_flight(flight_item: QFlightItem) -> timedelta:
|
def mission_start_for_flight(flight_item: QFlightItem) -> datetime:
|
||||||
return flight_item.flight.flight_plan.startup_time()
|
return flight_item.flight.flight_plan.startup_time()
|
||||||
|
|||||||
@ -57,7 +57,9 @@ class FlightAirfieldDisplay(QGroupBox):
|
|||||||
# handler may be called for a flight whose package has been canceled, which
|
# handler may be called for a flight whose package has been canceled, which
|
||||||
# is an invalid state for calling anything in TotEstimator.
|
# is an invalid state for calling anything in TotEstimator.
|
||||||
return
|
return
|
||||||
self.departure_time.setText(f"At T+{self.flight.flight_plan.startup_time()}")
|
self.departure_time.setText(
|
||||||
|
f"At {self.flight.flight_plan.startup_time():%H:%M%S}"
|
||||||
|
)
|
||||||
|
|
||||||
def set_divert(self, index: int) -> None:
|
def set_divert(self, index: int) -> None:
|
||||||
old_divert = self.flight.divert
|
old_divert = self.flight.divert
|
||||||
|
|||||||
@ -1,13 +1,11 @@
|
|||||||
from datetime import timedelta
|
|
||||||
|
|
||||||
from PySide6.QtCore import QItemSelectionModel, QPoint
|
from PySide6.QtCore import QItemSelectionModel, QPoint
|
||||||
from PySide6.QtGui import QStandardItem, QStandardItemModel
|
from PySide6.QtGui import QStandardItem, QStandardItemModel
|
||||||
from PySide6.QtWidgets import QHeaderView, QTableView
|
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.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
|
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)
|
time = flight.flight_plan.depart_time_for_waypoint(waypoint)
|
||||||
if time is None:
|
if time is None:
|
||||||
return ""
|
return ""
|
||||||
time = timedelta(seconds=int(time.total_seconds()))
|
return f"{prefix}{time:%H:%M:%S}"
|
||||||
return f"{prefix}T+{time}"
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def takeoff_text(flight: Flight) -> str:
|
def takeoff_text(flight: Flight) -> str:
|
||||||
takeoff_time = flight.flight_plan.takeoff_time()
|
return f"{flight.flight_plan.takeoff_time():%H:%M:%S}"
|
||||||
# Handle custom flight plans where we can't estimate the takeoff time.
|
|
||||||
if takeoff_time is None:
|
|
||||||
takeoff_time = timedelta()
|
|
||||||
start_time = timedelta(seconds=int(takeoff_time.total_seconds()))
|
|
||||||
return f"T+{start_time}"
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user