From d31f0e22e3035fa3e8f91224eb22a71ce1ae9a08 Mon Sep 17 00:00:00 2001 From: Dan Albert Date: Sun, 7 Nov 2021 01:24:49 -0700 Subject: [PATCH] Show the status of each flight in the UI. https://github.com/dcs-liberation/dcs_liberation/issues/1704 --- game/ato/flightstate/completed.py | 4 +++ game/ato/flightstate/flightstate.py | 6 ++++ game/ato/flightstate/inflight.py | 4 +++ game/ato/flightstate/loiter.py | 4 +++ game/ato/flightstate/racetrack.py | 4 +++ game/ato/flightstate/startup.py | 4 +++ game/ato/flightstate/takeoff.py | 4 +++ game/ato/flightstate/taxi.py | 4 +++ game/ato/flightstate/uninitialized.py | 7 +++++ game/ato/flightstate/waitingforstart.py | 4 +++ game/sim/gameloop.py | 6 +++- qt_ui/models.py | 22 ++++++++++---- qt_ui/simcontroller.py | 8 +++++- qt_ui/widgets/ato.py | 38 ++++++++++++++++--------- qt_ui/windows/QLiberationWindow.py | 4 +-- 15 files changed, 99 insertions(+), 24 deletions(-) diff --git a/game/ato/flightstate/completed.py b/game/ato/flightstate/completed.py index 18004e66..5fe74c28 100644 --- a/game/ato/flightstate/completed.py +++ b/game/ato/flightstate/completed.py @@ -16,3 +16,7 @@ class Completed(FlightState): def spawn_type(self) -> StartType: # TODO: May want to do something different to make these uncontrolled? return StartType.COLD + + @property + def description(self) -> str: + return "Completed" diff --git a/game/ato/flightstate/flightstate.py b/game/ato/flightstate/flightstate.py index 41ddf48c..60e83109 100644 --- a/game/ato/flightstate/flightstate.py +++ b/game/ato/flightstate/flightstate.py @@ -43,3 +43,9 @@ class FlightState(ABC): if (max_takeoff_fuel := self.flight.max_takeoff_fuel()) is not None: return max_takeoff_fuel return self.flight.unit_type.dcs_unit_type.fuel_max + + @property + @abstractmethod + def description(self) -> str: + """Describes the current flight state.""" + ... diff --git a/game/ato/flightstate/inflight.py b/game/ato/flightstate/inflight.py index dd65a7f8..96985157 100644 --- a/game/ato/flightstate/inflight.py +++ b/game/ato/flightstate/inflight.py @@ -170,3 +170,7 @@ class InFlight(FlightState): @property def spawn_type(self) -> StartType: return StartType.IN_FLIGHT + + @property + def description(self) -> str: + return f"Flying to {self.next_waypoint.name}" diff --git a/game/ato/flightstate/loiter.py b/game/ato/flightstate/loiter.py index 11342fc1..03301d24 100644 --- a/game/ato/flightstate/loiter.py +++ b/game/ato/flightstate/loiter.py @@ -40,3 +40,7 @@ class Loiter(InFlight): def travel_time_between_waypoints(self) -> timedelta: return self.hold_duration + + @property + def description(self) -> str: + return f"Loitering for {self.hold_duration - self.elapsed_time}" diff --git a/game/ato/flightstate/racetrack.py b/game/ato/flightstate/racetrack.py index 45301cc2..75bc47f5 100644 --- a/game/ato/flightstate/racetrack.py +++ b/game/ato/flightstate/racetrack.py @@ -51,3 +51,7 @@ class RaceTrack(InFlight): if self.flight.flight_type in {FlightType.BARCAP, FlightType.TARCAP}: return self.commit_region return None + + @property + def description(self) -> str: + return f"Patrolling for {self.patrol_duration - self.elapsed_time}" diff --git a/game/ato/flightstate/startup.py b/game/ato/flightstate/startup.py index df94aef0..6503b31c 100644 --- a/game/ato/flightstate/startup.py +++ b/game/ato/flightstate/startup.py @@ -43,3 +43,7 @@ class StartUp(FlightState): ) return True return False + + @property + def description(self) -> str: + return "Starting up" diff --git a/game/ato/flightstate/takeoff.py b/game/ato/flightstate/takeoff.py index 905caa89..ee56a529 100644 --- a/game/ato/flightstate/takeoff.py +++ b/game/ato/flightstate/takeoff.py @@ -51,3 +51,7 @@ class Takeoff(FlightState): ) return True return False + + @property + def description(self) -> str: + return "Taking off" diff --git a/game/ato/flightstate/taxi.py b/game/ato/flightstate/taxi.py index e5d29abf..e52cb84a 100644 --- a/game/ato/flightstate/taxi.py +++ b/game/ato/flightstate/taxi.py @@ -43,3 +43,7 @@ class Taxi(FlightState): ) return True return False + + @property + def description(self) -> str: + return "Taxiing" diff --git a/game/ato/flightstate/uninitialized.py b/game/ato/flightstate/uninitialized.py index 9ac6ffaf..a94dfdf5 100644 --- a/game/ato/flightstate/uninitialized.py +++ b/game/ato/flightstate/uninitialized.py @@ -1,5 +1,6 @@ from datetime import datetime, timedelta +from gen.flights.traveltime import TotEstimator from .flightstate import FlightState from ..starttype import StartType @@ -15,3 +16,9 @@ class Uninitialized(FlightState): @property def spawn_type(self) -> StartType: raise RuntimeError("Attempted to simulate flight that is not fully initialized") + + @property + def description(self) -> str: + estimator = TotEstimator(self.flight.package) + delay = estimator.mission_start_time(self.flight) + return f"Starting in {delay}" diff --git a/game/ato/flightstate/waitingforstart.py b/game/ato/flightstate/waitingforstart.py index b748d31e..4463a725 100644 --- a/game/ato/flightstate/waitingforstart.py +++ b/game/ato/flightstate/waitingforstart.py @@ -54,3 +54,7 @@ class WaitingForStart(FlightState): @property def spawn_type(self) -> StartType: return self.flight.start_type + + @property + def description(self) -> str: + return f"Waiting for startup at {self.start_time:%H:%M:%S}" diff --git a/game/sim/gameloop.py b/game/sim/gameloop.py index 21a733de..c746cdf8 100644 --- a/game/sim/gameloop.py +++ b/game/sim/gameloop.py @@ -1,7 +1,7 @@ from __future__ import annotations import logging -from datetime import datetime +from datetime import datetime, timedelta from pathlib import Path from typing import Callable, TYPE_CHECKING @@ -27,6 +27,10 @@ class GameLoop: def current_time_in_sim(self) -> datetime: return self.sim.time + @property + def elapsed_time(self) -> timedelta: + return self.sim.time - self.game.conditions.start_time + def start(self) -> None: if self.started: raise RuntimeError("Cannot start game loop because it has already started") diff --git a/qt_ui/models.py b/qt_ui/models.py index 31325b84..75e30409 100644 --- a/qt_ui/models.py +++ b/qt_ui/models.py @@ -12,15 +12,16 @@ from PySide2.QtCore import ( ) from PySide2.QtGui import QIcon +from game.ato.airtaaskingorder import AirTaskingOrder +from game.ato.flight import Flight +from game.ato.flighttype import FlightType +from game.ato.package import Package from game.game import Game from game.squadrons.squadron import Pilot, Squadron from game.theater.missiontarget import MissionTarget -from game.transfers import TransferOrder, PendingTransfers -from game.ato.airtaaskingorder import AirTaskingOrder -from game.ato.package import Package -from game.ato.flighttype import FlightType -from game.ato.flight import Flight +from game.transfers import PendingTransfers, TransferOrder from gen.flights.traveltime import TotEstimator +from qt_ui.simcontroller import SimController from qt_ui.uiconstants import AIRCRAFT_ICONS @@ -116,6 +117,7 @@ class PackageModel(QAbstractListModel): super().__init__() self.package = package self.game_model = game_model + self.game_model.sim_controller.sim_update.connect(self.on_sim_update) def rowCount(self, parent: QModelIndex = QModelIndex()) -> int: return len(self.package.flights) @@ -209,6 +211,9 @@ class PackageModel(QAbstractListModel): for flight in self.package.flights: yield flight + def on_sim_update(self) -> None: + self.dataChanged.emit(self.index(0), self.index(self.rowCount())) + class AtoModel(QAbstractListModel): """The model for an AirTaskingOrder.""" @@ -222,6 +227,7 @@ class AtoModel(QAbstractListModel): self.game_model = game_model self.ato = ato self.package_models = DeletableChildModelManager(PackageModel, game_model) + self.game_model.sim_controller.sim_update.connect(self.on_sim_update) @property def game(self) -> Optional[Game]: @@ -305,6 +311,9 @@ class AtoModel(QAbstractListModel): for package in self.ato.packages: yield self.package_models.acquire(package) + def on_sim_update(self) -> None: + self.dataChanged.emit(self.index(0), self.index(self.rowCount())) + class TransferModel(QAbstractListModel): """The model for a ground unit transfer.""" @@ -483,8 +492,9 @@ class GameModel: its ATO objects. """ - def __init__(self, game: Optional[Game]) -> None: + def __init__(self, game: Optional[Game], sim_controller: SimController) -> None: self.game: Optional[Game] = game + self.sim_controller = sim_controller self.transfer_model = TransferModel(self) self.blue_air_wing_model = AirWingModel(self, player=True) if self.game is None: diff --git a/qt_ui/simcontroller.py b/qt_ui/simcontroller.py index fbfb88b5..9df07aa1 100644 --- a/qt_ui/simcontroller.py +++ b/qt_ui/simcontroller.py @@ -1,7 +1,7 @@ from __future__ import annotations import logging -from datetime import datetime +from datetime import datetime, timedelta from pathlib import Path from typing import Callable, Optional, TYPE_CHECKING @@ -40,6 +40,12 @@ class SimController(QObject): return None return self.game_loop.current_time_in_sim + @property + def elapsed_time(self) -> timedelta: + if self.game_loop is None: + return timedelta() + return self.game_loop.elapsed_time + def set_game(self, game: Optional[Game]) -> None: self.recreate_game_loop(game) self.sim_speed_reset.emit(SimSpeedSetting.PAUSED) diff --git a/qt_ui/widgets/ato.py b/qt_ui/widgets/ato.py index 3443d54a..1104e790 100644 --- a/qt_ui/widgets/ato.py +++ b/qt_ui/widgets/ato.py @@ -1,5 +1,6 @@ """Widgets for displaying air tasking orders.""" import logging +from datetime import timedelta from typing import Optional from PySide2.QtCore import ( @@ -24,9 +25,8 @@ from PySide2.QtWidgets import ( QVBoxLayout, ) -from game.ato.package import Package from game.ato.flight import Flight -from gen.flights.traveltime import TotEstimator +from game.ato.package import Package from qt_ui.windows.GameUpdateSignal import GameUpdateSignal from ..delegates import TwoColumnRowDelegate from ..models import AtoModel, GameModel, NullListModel, PackageModel @@ -34,7 +34,7 @@ from ..models import AtoModel, GameModel, NullListModel, PackageModel class FlightDelegate(TwoColumnRowDelegate): def __init__(self, package: Package) -> None: - super().__init__(rows=2, columns=2, font_size=10) + super().__init__(rows=3, columns=2, font_size=10) self.package = package @staticmethod @@ -44,9 +44,7 @@ class FlightDelegate(TwoColumnRowDelegate): def text_for(self, index: QModelIndex, row: int, column: int) -> str: flight = self.flight(index) if (row, column) == (0, 0): - estimator = TotEstimator(self.package) - delay = estimator.mission_start_time(flight) - return f"{flight} in {delay}" + return f"{flight}" elif (row, column) == (0, 1): clients = self.num_clients(index) return f"Player Slots: {clients}" if clients else "" @@ -58,6 +56,8 @@ class FlightDelegate(TwoColumnRowDelegate): elif (row, column) == (1, 1): missing_pilots = flight.missing_pilots return f"Missing pilots: {flight.missing_pilots}" if missing_pilots else "" + elif (row, column) == (2, 0): + return flight.state.description return "" def num_clients(self, index: QModelIndex) -> int: @@ -235,8 +235,9 @@ class QFlightPanel(QGroupBox): class PackageDelegate(TwoColumnRowDelegate): - def __init__(self) -> None: + def __init__(self, game_model: GameModel) -> None: super().__init__(rows=2, columns=2) + self.game_model = game_model @staticmethod def package(index: QModelIndex) -> Package: @@ -250,7 +251,16 @@ class PackageDelegate(TwoColumnRowDelegate): clients = self.num_clients(index) return f"Player Slots: {clients}" if clients else "" elif (row, column) == (1, 0): - return f"TOT T+{package.time_over_target}" + 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}" elif (row, column) == (1, 1): unassigned_pilots = self.missing_pilots(index) return f"Missing pilots: {unassigned_pilots}" if unassigned_pilots else "" @@ -268,11 +278,11 @@ class PackageDelegate(TwoColumnRowDelegate): class QPackageList(QListView): """List view for displaying the packages of an ATO.""" - def __init__(self, model: AtoModel) -> None: + def __init__(self, game_model: GameModel, model: AtoModel) -> None: super().__init__() self.ato_model = model self.setModel(model) - self.setItemDelegate(PackageDelegate()) + self.setItemDelegate(PackageDelegate(game_model)) self.setIconSize(QSize(0, 0)) self.setSelectionBehavior(QAbstractItemView.SelectItems) self.model().rowsInserted.connect(self.on_new_packages) @@ -331,9 +341,9 @@ class QPackagePanel(QGroupBox): delete buttons for package management. """ - def __init__(self, model: AtoModel) -> None: + def __init__(self, game_model: GameModel, ato_model: AtoModel) -> None: super().__init__("Packages") - self.ato_model = model + self.ato_model = ato_model self.ato_model.layoutChanged.connect(self.on_current_changed) self.vbox = QVBoxLayout() @@ -346,7 +356,7 @@ class QPackagePanel(QGroupBox): ) self.vbox.addWidget(self.tip) - self.package_list = QPackageList(self.ato_model) + self.package_list = QPackageList(game_model, self.ato_model) self.vbox.addWidget(self.package_list) self.button_row = QHBoxLayout() @@ -418,7 +428,7 @@ class QAirTaskingOrderPanel(QSplitter): super().__init__(Qt.Vertical) self.ato_model = game_model.ato_model - self.package_panel = QPackagePanel(self.ato_model) + self.package_panel = QPackagePanel(game_model, self.ato_model) self.package_panel.current_changed.connect(self.on_package_change) self.addWidget(self.package_panel) diff --git a/qt_ui/windows/QLiberationWindow.py b/qt_ui/windows/QLiberationWindow.py index 05f5564a..59dc1313 100644 --- a/qt_ui/windows/QLiberationWindow.py +++ b/qt_ui/windows/QLiberationWindow.py @@ -49,9 +49,9 @@ class QLiberationWindow(QMainWindow): self._uncaught_exception_handler = UncaughtExceptionHandler(self) self.game = game - self.game_model = GameModel(game) - Dialog.set_game(self.game_model) self.sim_controller = SimController(self.game) + self.game_model = GameModel(game, self.sim_controller) + Dialog.set_game(self.game_model) self.ato_panel = QAirTaskingOrderPanel(self.game_model) self.info_panel = QInfoPanel(self.game) self.liberation_map = QLiberationMap(self.game_model, self.sim_controller, self)