Convert TOTs to datetime.

https://github.com/dcs-liberation/dcs_liberation/issues/1680
This commit is contained in:
Dan Albert 2022-09-02 20:58:26 -07:00
parent ac6cc39616
commit fd2ba6b2b2
51 changed files with 293 additions and 273 deletions

View File

@ -1,7 +1,7 @@
from __future__ import annotations
from dataclasses import dataclass
from datetime import timedelta
from datetime import datetime
from typing import Iterator, TYPE_CHECKING, Type
from game.ato.flightplans.standard import StandardFlightPlan, StandardLayout
@ -55,12 +55,12 @@ class AirAssaultFlightPlan(StandardFlightPlan[AirAssaultLayout], UiZoneDisplay):
def tot_waypoint(self) -> FlightWaypoint:
return self.layout.drop_off
def tot_for_waypoint(self, waypoint: FlightWaypoint) -> timedelta | None:
def tot_for_waypoint(self, waypoint: FlightWaypoint) -> datetime | None:
if waypoint == self.tot_waypoint:
return self.tot
return None
def depart_time_for_waypoint(self, waypoint: FlightWaypoint) -> timedelta | None:
def depart_time_for_waypoint(self, waypoint: FlightWaypoint) -> datetime | None:
return None
@property
@ -68,11 +68,11 @@ class AirAssaultFlightPlan(StandardFlightPlan[AirAssaultLayout], UiZoneDisplay):
return meters(2500)
@property
def mission_begin_on_station_time(self) -> timedelta | None:
def mission_begin_on_station_time(self) -> datetime | None:
return None
@property
def mission_departure_time(self) -> timedelta:
def mission_departure_time(self) -> datetime:
return self.package.time_over_target
def ui_zone(self) -> UiZone:

View File

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

View File

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

View File

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

View File

@ -11,7 +11,7 @@ import math
from abc import ABC, abstractmethod
from collections.abc import Iterator
from dataclasses import dataclass
from datetime import timedelta
from datetime import datetime, timedelta
from functools import cached_property
from typing import Any, Generic, TYPE_CHECKING, TypeGuard, TypeVar
@ -149,7 +149,7 @@ class FlightPlan(ABC, Generic[LayoutT]):
raise NotImplementedError
@property
def tot(self) -> timedelta:
def tot(self) -> datetime:
return self.package.time_over_target + self.tot_offset
@cached_property
@ -215,7 +215,13 @@ class FlightPlan(ABC, Generic[LayoutT]):
for previous_waypoint, waypoint in self.edges(until=destination):
total += self.travel_time_between_waypoints(previous_waypoint, waypoint)
return total
# Trim microseconds. Our simulation tick rate is 1 second, so anything that
# takes 100.1 or 100.9 seconds will take 100 seconds. DCS doesn't handle
# sub-second resolution for tasks anyway, nor are they interesting from a
# mission planning perspective, so there's little value to keeping them in the
# model.
return timedelta(seconds=math.floor(total.total_seconds()))
def travel_time_between_waypoints(
self, a: FlightWaypoint, b: FlightWaypoint
@ -224,10 +230,10 @@ class FlightPlan(ABC, Generic[LayoutT]):
a.position, b.position, self.speed_between_waypoints(a, b)
)
def tot_for_waypoint(self, waypoint: FlightWaypoint) -> timedelta | None:
def tot_for_waypoint(self, waypoint: FlightWaypoint) -> datetime | None:
raise NotImplementedError
def depart_time_for_waypoint(self, waypoint: FlightWaypoint) -> timedelta | None:
def depart_time_for_waypoint(self, waypoint: FlightWaypoint) -> datetime | None:
raise NotImplementedError
def request_escort_at(self) -> FlightWaypoint | None:
@ -250,34 +256,20 @@ class FlightPlan(ABC, Generic[LayoutT]):
if waypoint == end:
return
def takeoff_time(self) -> timedelta:
def takeoff_time(self) -> datetime:
return self.tot - self._travel_time_to_waypoint(self.tot_waypoint)
def startup_time(self) -> timedelta:
start_time = (
self.takeoff_time() - self.estimate_startup() - self.estimate_ground_ops()
def minimum_duration_from_start_to_tot(self) -> timedelta:
return (
self._travel_time_to_waypoint(self.tot_waypoint)
+ self.estimate_startup()
+ self.estimate_ground_ops()
)
# In case FP math has given us some barely below zero time, round to
# zero.
if math.isclose(start_time.total_seconds(), 0):
start_time = timedelta()
# Trim microseconds. DCS doesn't handle sub-second resolution for tasks,
# and they're not interesting from a mission planning perspective so we
# don't want them in the UI.
#
# Round down so *barely* above zero start times are just zero.
start_time = timedelta(seconds=math.floor(start_time.total_seconds()))
# Feature request #1309: Carrier planes should start at +1s
# This is a workaround to a DCS problem: some AI planes spawn on
# the 'sixpack' when start_time is zero and cause a deadlock.
# Workaround: force the start_time to 1 second for these planes.
if self.flight.from_cp.is_fleet and start_time.total_seconds() == 0:
start_time = timedelta(seconds=1)
return start_time
def startup_time(self) -> datetime:
return (
self.takeoff_time() - self.estimate_startup() - self.estimate_ground_ops()
)
def estimate_startup(self) -> timedelta:
if self.flight.start_type is StartType.COLD:
@ -298,7 +290,7 @@ class FlightPlan(ABC, Generic[LayoutT]):
@property
@abstractmethod
def mission_begin_on_station_time(self) -> timedelta | None:
def mission_begin_on_station_time(self) -> datetime | None:
"""The time that the mission is first on-station.
Not all mission types will have a time when they can be considered on-station.
@ -307,7 +299,7 @@ class FlightPlan(ABC, Generic[LayoutT]):
"""
@property
def mission_departure_time(self) -> timedelta:
def mission_departure_time(self) -> datetime:
"""The time that the mission is complete and the flight RTBs."""
raise NotImplementedError

View File

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

View File

@ -3,7 +3,7 @@ from __future__ import annotations
from abc import ABC
from collections.abc import Iterator
from dataclasses import dataclass
from datetime import timedelta
from datetime import datetime, timedelta
from typing import TYPE_CHECKING, TypeVar
from dcs import Point
@ -91,14 +91,14 @@ class FormationAttackFlightPlan(FormationFlightPlan, ABC):
return total
@property
def join_time(self) -> timedelta:
def join_time(self) -> datetime:
travel_time = self.travel_time_between_waypoints(
self.layout.join, self.layout.ingress
)
return self.ingress_time - travel_time
@property
def split_time(self) -> timedelta:
def split_time(self) -> datetime:
travel_time_ingress = self.travel_time_between_waypoints(
self.layout.ingress, self.target_area_waypoint
)
@ -115,14 +115,14 @@ class FormationAttackFlightPlan(FormationFlightPlan, ABC):
)
@property
def ingress_time(self) -> timedelta:
def ingress_time(self) -> datetime:
tot = self.tot
travel_time = self.travel_time_between_waypoints(
self.layout.ingress, self.target_area_waypoint
)
return tot - travel_time
def tot_for_waypoint(self, waypoint: FlightWaypoint) -> timedelta | None:
def tot_for_waypoint(self, waypoint: FlightWaypoint) -> datetime | None:
if waypoint == self.layout.ingress:
return self.ingress_time
elif waypoint in self.layout.targets:

View File

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

View File

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

View File

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

View File

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

View File

@ -1,7 +1,7 @@
from __future__ import annotations
from dataclasses import dataclass
from datetime import timedelta
from datetime import datetime, timedelta
from typing import Iterator, Type
from game.ato.flightplans.ibuilder import IBuilder
@ -37,19 +37,27 @@ class RecoveryTankerFlightPlan(StandardFlightPlan[RecoveryTankerLayout]):
return self.layout.recovery_ship
@property
def mission_begin_on_station_time(self) -> timedelta:
def mission_begin_on_station_time(self) -> datetime:
return self.package.time_over_target
@property
def mission_departure_time(self) -> timedelta:
def mission_departure_time(self) -> datetime:
return self.patrol_end_time
@property
def patrol_start_time(self) -> datetime:
return self.package.time_over_target
@property
def patrol_end_time(self) -> datetime:
return self.tot + timedelta(hours=2)
def tot_for_waypoint(self, waypoint: FlightWaypoint) -> timedelta | None:
def tot_for_waypoint(self, waypoint: FlightWaypoint) -> datetime | None:
if waypoint == self.tot_waypoint:
return self.tot
return None
def depart_time_for_waypoint(self, waypoint: FlightWaypoint) -> timedelta | None:
def depart_time_for_waypoint(self, waypoint: FlightWaypoint) -> datetime | None:
if waypoint == self.tot_waypoint:
return self.mission_departure_time
return None

View File

@ -1,7 +1,7 @@
from __future__ import annotations
from dataclasses import dataclass
from datetime import timedelta
from datetime import datetime, timedelta
from typing import Iterator, TYPE_CHECKING, Type
from dcs import Point
@ -59,30 +59,30 @@ class SweepFlightPlan(LoiterFlightPlan):
return -self.lead_time
@property
def sweep_start_time(self) -> timedelta:
def sweep_start_time(self) -> datetime:
travel_time = self.travel_time_between_waypoints(
self.layout.sweep_start, self.layout.sweep_end
)
return self.sweep_end_time - travel_time
@property
def sweep_end_time(self) -> timedelta:
def sweep_end_time(self) -> datetime:
return self.tot
def tot_for_waypoint(self, waypoint: FlightWaypoint) -> timedelta | None:
def tot_for_waypoint(self, waypoint: FlightWaypoint) -> datetime | None:
if waypoint == self.layout.sweep_start:
return self.sweep_start_time
if waypoint == self.layout.sweep_end:
return self.sweep_end_time
return None
def depart_time_for_waypoint(self, waypoint: FlightWaypoint) -> timedelta | None:
def depart_time_for_waypoint(self, waypoint: FlightWaypoint) -> datetime | None:
if waypoint == self.layout.hold:
return self.push_time
return None
@property
def push_time(self) -> timedelta:
def push_time(self) -> datetime:
return self.sweep_end_time - TravelTime.between_points(
self.layout.hold.position,
self.layout.sweep_end.position,
@ -90,10 +90,10 @@ class SweepFlightPlan(LoiterFlightPlan):
)
@property
def mission_begin_on_station_time(self) -> timedelta | None:
def mission_begin_on_station_time(self) -> datetime | None:
return None
def mission_departure_time(self) -> timedelta:
def mission_departure_time(self) -> datetime:
return self.sweep_end_time

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -22,8 +22,6 @@ class HoldPointBuilder(PydcsWaypointBuilder):
return
push_time = self.flight.flight_plan.push_time
self.waypoint.departure_time = push_time
loiter.stop_after_time(
int((push_time - self.elapsed_mission_time).total_seconds())
)
loiter.stop_after_time(int((push_time - self.now).total_seconds()))
waypoint.add_task(loiter)
waypoint.add_task(OptFormation.finger_four_close())

View File

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

View File

@ -56,7 +56,7 @@ class RaceTrackBuilder(PydcsWaypointBuilder):
racetrack = ControlledTask(orbit)
self.set_waypoint_tot(waypoint, flight_plan.patrol_start_time)
loiter_duration = flight_plan.patrol_end_time - self.elapsed_mission_time
loiter_duration = flight_plan.patrol_end_time - self.now
racetrack.stop_after_time(int(loiter_duration.total_seconds()))
waypoint.add_task(racetrack)

View File

@ -25,13 +25,13 @@ from game.settings import Settings
from game.unitmap import UnitMap
from game.utils import pairwise
from .baiingress import BaiIngressBuilder
from .landingzone import LandingZoneBuilder
from .casingress import CasIngressBuilder
from .deadingress import DeadIngressBuilder
from .default import DefaultWaypointBuilder
from .holdpoint import HoldPointBuilder
from .joinpoint import JoinPointBuilder
from .landingpoint import LandingPointBuilder
from .landingzone import LandingZoneBuilder
from .ocaaircraftingress import OcaAircraftIngressBuilder
from .ocarunwayingress import OcaRunwayIngressBuilder
from .pydcswaypointbuilder import PydcsWaypointBuilder, TARGET_WAYPOINTS
@ -50,7 +50,6 @@ class WaypointGenerator:
flight: Flight,
group: FlyingGroup[Any],
mission: Mission,
turn_start_time: datetime,
time: datetime,
settings: Settings,
mission_data: MissionData,
@ -59,7 +58,6 @@ class WaypointGenerator:
self.flight = flight
self.group = group
self.mission = mission
self.elapsed_mission_time = time - turn_start_time
self.time = time
self.settings = settings
self.mission_data = mission_data
@ -150,7 +148,7 @@ class WaypointGenerator:
self.group,
self.flight,
self.mission,
self.elapsed_mission_time,
self.time,
self.mission_data,
self.unit_map,
)
@ -182,12 +180,29 @@ class WaypointGenerator:
a.min_fuel = min_fuel
def set_takeoff_time(self, waypoint: FlightWaypoint) -> timedelta:
force_delay = False
if isinstance(self.flight.state, WaitingForStart):
delay = self.flight.state.time_remaining(self.time)
elif (
# The first two clauses capture the flight states that we want to adjust. We
# don't want to delay any flights that are already in flight or on the
# runway.
not self.flight.state.in_flight
and self.flight.state.spawn_type is not StartType.RUNWAY
and self.flight.departure.is_fleet
and not self.flight.client_count
):
# https://github.com/dcs-liberation/dcs_liberation/issues/1309
# Without a delay, AI aircraft will be spawned on the sixpack, which other
# AI planes of course want to taxi through, deadlocking the carrier deck.
# Delaying AI carrier deck spawns by one second for some reason causes DCS
# to spawn those aircraft elsewhere, avoiding the traffic jam.
delay = timedelta(seconds=1)
force_delay = True
else:
delay = timedelta()
if self.should_delay_flight():
if force_delay or self.should_delay_flight():
if self.should_activate_late():
# Late activation causes the aircraft to not be spawned
# until triggered.

View File

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

View File

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

View File

@ -1,11 +1,11 @@
from __future__ import annotations
from dataclasses import dataclass, field
from datetime import timedelta
from datetime import datetime
from typing import Optional, TYPE_CHECKING
from game.dcs.aircrafttype import AircraftType
from game.missiongenerator.aircraft.flightdata import FlightData
from game.runways import RunwayData
if TYPE_CHECKING:
@ -31,8 +31,8 @@ class AwacsInfo(GroupInfo):
"""AWACS information for the kneeboard."""
depature_location: Optional[str]
start_time: Optional[timedelta]
end_time: Optional[timedelta]
start_time: datetime | None
end_time: datetime | None
@dataclass
@ -41,8 +41,8 @@ class TankerInfo(GroupInfo):
variant: str
tacan: TacanChannel
start_time: Optional[timedelta]
end_time: Optional[timedelta]
start_time: datetime | None
end_time: datetime | None
@dataclass

View File

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

View File

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

View File

@ -4,6 +4,7 @@ import logging
import random
from collections.abc import Iterable
from dataclasses import dataclass, field
from datetime import datetime
from typing import Optional, Sequence, TYPE_CHECKING
from faker import Faker
@ -334,7 +335,7 @@ class Squadron:
def arrival(self) -> ControlPoint:
return self.location if self.destination is None else self.destination
def plan_relocation(self, destination: ControlPoint) -> None:
def plan_relocation(self, destination: ControlPoint, now: datetime) -> None:
if destination == self.location:
logging.warning(
f"Attempted to plan relocation of {self} to current location "
@ -353,7 +354,7 @@ class Squadron:
if not destination.can_operate(self.aircraft):
raise RuntimeError(f"{self} cannot operate at {destination}.")
self.destination = destination
self.replan_ferry_flights()
self.replan_ferry_flights(now)
def cancel_relocation(self) -> None:
if self.destination is None:
@ -368,9 +369,9 @@ class Squadron:
self.destination = None
self.cancel_ferry_flights()
def replan_ferry_flights(self) -> None:
def replan_ferry_flights(self, now: datetime) -> None:
self.cancel_ferry_flights()
self.plan_ferry_flights()
self.plan_ferry_flights(now)
def cancel_ferry_flights(self) -> None:
for package in self.coalition.ato.packages:
@ -381,7 +382,7 @@ class Squadron:
if not package.flights:
self.coalition.ato.remove_package(package)
def plan_ferry_flights(self) -> None:
def plan_ferry_flights(self, now: datetime) -> None:
if self.destination is None:
raise RuntimeError(
f"Cannot plan ferry flights for {self} because there is no destination."
@ -395,7 +396,7 @@ class Squadron:
size = min(remaining, self.aircraft.max_group_size)
self.plan_ferry_flight(package, size)
remaining -= size
package.set_tot_asap()
package.set_tot_asap(now)
self.coalition.ato.add_package(package)
def plan_ferry_flight(self, package: Package, size: int) -> None:

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,3 +1,4 @@
from datetime import datetime
from typing import List, Optional
from PySide6.QtWidgets import (
@ -155,13 +156,14 @@ class QTopPanel(QFrame):
GameUpdateSignal.get_instance().updateGame(self.game)
self.proceedButton.setEnabled(True)
def negative_start_packages(self) -> List[Package]:
def negative_start_packages(self, now: datetime) -> List[Package]:
packages = []
for package in self.game_model.ato_model.ato.packages:
if not package.flights:
continue
for flight in package.flights:
if flight.flight_plan.startup_time().total_seconds() < 0:
startup = flight.flight_plan.startup_time()
if startup < now:
packages.append(package)
break
return packages
@ -277,7 +279,9 @@ class QTopPanel(QFrame):
if self.check_no_missing_pilots():
return
negative_starts = self.negative_start_packages()
negative_starts = self.negative_start_packages(
self.sim_controller.current_time_in_sim
)
if negative_starts:
if not self.confirm_negative_start_time(negative_starts):
return

View File

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

View File

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

View File

@ -20,6 +20,7 @@ from game.theater import ConflictTheater, ControlPoint
from qt_ui.delegates import TwoColumnRowDelegate
from qt_ui.errorreporter import report_errors
from qt_ui.models import AtoModel, SquadronModel
from qt_ui.simcontroller import SimController
class PilotDelegate(TwoColumnRowDelegate):
@ -134,11 +135,13 @@ class SquadronDialog(QDialog):
ato_model: AtoModel,
squadron_model: SquadronModel,
theater: ConflictTheater,
sim_controller: SimController,
parent,
) -> None:
super().__init__(parent)
self.ato_model = ato_model
self.squadron_model = squadron_model
self.sim_controller = sim_controller
self.setMinimumSize(1000, 440)
self.setWindowTitle(str(squadron_model.squadron))
@ -194,7 +197,9 @@ class SquadronDialog(QDialog):
if destination is None:
self.squadron.cancel_relocation()
else:
self.squadron.plan_relocation(destination)
self.squadron.plan_relocation(
destination, self.sim_controller.current_time_in_sim
)
self.ato_model.replace_from_game(player=True)
def check_disabled_button_states(

View File

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

View File

@ -16,4 +16,4 @@ class QFlightItem(QStandardItem):
icon = QIcon((AIRCRAFT_ICONS[self.flight.unit_type.dcs_id]))
self.setIcon(icon)
self.setEditable(False)
self.setText(f"{flight} in {flight.flight_plan.startup_time()}")
self.setText(f"{flight} at {flight.flight_plan.startup_time()}")

View File

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

View File

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

View File

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

View File

@ -1,13 +1,11 @@
from datetime import timedelta
from PySide6.QtCore import QItemSelectionModel, QPoint
from PySide6.QtGui import QStandardItem, QStandardItemModel
from PySide6.QtWidgets import QHeaderView, QTableView
from game.ato.package import Package
from game.ato.flightwaypointtype import FlightWaypointType
from game.ato.flightwaypoint import FlightWaypoint
from game.ato.flight import Flight
from game.ato.flightwaypoint import FlightWaypoint
from game.ato.flightwaypointtype import FlightWaypointType
from game.ato.package import Package
from qt_ui.windows.mission.flight.waypoints.QFlightWaypointItem import QWaypointItem
@ -77,14 +75,8 @@ class QFlightWaypointList(QTableView):
time = flight.flight_plan.depart_time_for_waypoint(waypoint)
if time is None:
return ""
time = timedelta(seconds=int(time.total_seconds()))
return f"{prefix}T+{time}"
return f"{prefix}{time:%H:%M:%S}"
@staticmethod
def takeoff_text(flight: Flight) -> str:
takeoff_time = flight.flight_plan.takeoff_time()
# Handle custom flight plans where we can't estimate the takeoff time.
if takeoff_time is None:
takeoff_time = timedelta()
start_time = timedelta(seconds=int(takeoff_time.total_seconds()))
return f"T+{start_time}"
return f"{flight.flight_plan.takeoff_time():%H:%M:%S}"