Merge branch 'squadron-relocation' into develop

This commit is contained in:
Dan Albert 2021-08-29 16:22:02 -07:00
commit 18eb661e84
9 changed files with 194 additions and 44 deletions

View File

@ -53,6 +53,9 @@ class Squadron:
settings: Settings = field(hash=False, compare=False)
location: ControlPoint
destination: Optional[ControlPoint] = field(
init=False, hash=False, compare=False, default=None
)
owned_aircraft: int = field(init=False, hash=False, compare=False, default=0)
untasked_aircraft: int = field(init=False, hash=False, compare=False, default=0)
@ -168,6 +171,8 @@ class Squadron:
self._recruit_pilots(self.settings.squadron_pilot_limit)
def end_turn(self) -> None:
if self.destination is not None:
self.relocate_to(self.destination)
self.replenish_lost_pilots()
self.deliver_orders()
@ -280,6 +285,8 @@ class Squadron:
def relocate_to(self, destination: ControlPoint) -> None:
self.location = destination
if self.location == self.destination:
self.destination = None
def cancel_overflow_orders(self) -> None:
if self.pending_deliveries <= 0:
@ -297,6 +304,42 @@ class Squadron:
def max_fulfillable_aircraft(self) -> int:
return max(self.number_of_available_pilots, self.untasked_aircraft)
@property
def expected_size_next_turn(self) -> int:
return self.owned_aircraft + self.pending_deliveries
def plan_relocation(self, destination: ControlPoint) -> None:
if destination == self.location:
logging.warning(
f"Attempted to plan relocation of {self} to current location "
f"{destination}. Ignoring."
)
return
if destination == self.destination:
logging.warning(
f"Attempted to plan relocation of {self} to current destination "
f"{destination}. Ignoring."
)
return
if self.expected_size_next_turn >= destination.unclaimed_parking():
raise RuntimeError(f"Not enough parking for {self} at {destination}.")
if not destination.can_operate(self.aircraft):
raise RuntimeError(f"{self} cannot operate at {destination}.")
self.destination = destination
def cancel_relocation(self) -> None:
if self.destination is None:
logging.warning(
f"Attempted to cancel relocation of squadron with no transfer order. "
"Ignoring."
)
return
if self.expected_size_next_turn >= self.location.unclaimed_parking():
raise RuntimeError(f"Not enough parking for {self} at {self.location}.")
self.destination = None
@classmethod
def create_from(
cls,

View File

@ -728,13 +728,19 @@ class ControlPoint(MissionTarget, ABC):
def allocated_aircraft(self) -> AircraftAllocations:
present: dict[AircraftType, int] = defaultdict(int)
on_order: dict[AircraftType, int] = defaultdict(int)
transferring: dict[AircraftType, int] = defaultdict(int)
for squadron in self.squadrons:
present[squadron.aircraft] += squadron.owned_aircraft
# TODO: Only if this is the squadron destination, not location.
on_order[squadron.aircraft] += squadron.pending_deliveries
if squadron.destination is None:
on_order[squadron.aircraft] += squadron.pending_deliveries
else:
transferring[squadron.aircraft] -= squadron.owned_aircraft
for squadron in self.coalition.air_wing.iter_squadrons():
if squadron.destination == self:
on_order[squadron.aircraft] += squadron.pending_deliveries
transferring[squadron.aircraft] += squadron.owned_aircraft
# TODO: Implement squadron transfers.
return AircraftAllocations(present, on_order, transferring={})
return AircraftAllocations(present, on_order, transferring)
def allocated_ground_units(
self, transfers: PendingTransfers

17
qt_ui/errorreporter.py Normal file
View File

@ -0,0 +1,17 @@
import logging
from collections import Iterator
from contextlib import contextmanager
from typing import Type
from PySide2.QtWidgets import QDialog, QMessageBox
@contextmanager
def report_errors(
title: str, parent: QDialog, error_type: Type[Exception] = Exception
) -> Iterator[None]:
try:
yield
except error_type as ex:
logging.exception(title)
QMessageBox().critical(parent, title, str(ex), QMessageBox.Ok)

View File

@ -13,6 +13,7 @@ from PySide2.QtWidgets import QApplication, QSplashScreen
from dcs.payloads import PayloadDirectories
from game import Game, VERSION, persistency
from game.campaignloader.campaign import Campaign
from game.data.weapons import WeaponGroup, Pylon, Weapon
from game.db import FACTIONS
from game.dcs.aircrafttype import AircraftType
@ -27,7 +28,6 @@ from qt_ui import (
)
from qt_ui.windows.GameUpdateSignal import GameUpdateSignal
from qt_ui.windows.QLiberationWindow import QLiberationWindow
from game.campaignloader.campaign import Campaign
from qt_ui.windows.newgame.QNewGameWizard import DEFAULT_BUDGET
from qt_ui.windows.preferences.QLiberationFirstStartWindow import (
QLiberationFirstStartWindow,

View File

@ -0,0 +1,42 @@
# From https://timlehr.com/python-exception-hooks-with-qt-message-box/
import logging
import sys
import traceback
from PySide2.QtCore import Signal, QObject
from PySide2.QtWidgets import QMessageBox, QApplication
class UncaughtExceptionHandler(QObject):
_exception_caught = Signal(str, str)
def __init__(self, parent: QObject):
super().__init__(parent)
sys.excepthook = self.exception_hook
# Use a signal so that the message box always comes from the main thread.
self._exception_caught.connect(self.show_exception_box)
def exception_hook(self, exc_type, exc_value, exc_traceback):
if issubclass(exc_type, KeyboardInterrupt):
# Ignore keyboard interrupt to support console applications.
sys.__excepthook__(exc_type, exc_value, exc_traceback)
return
logging.exception(
"Uncaught exception", exc_info=(exc_type, exc_value, exc_traceback)
)
self._exception_caught.emit(
str(exc_value),
"".join(traceback.format_exception(exc_type, exc_value, exc_traceback)),
)
def show_exception_box(self, message: str, exception: str) -> None:
if QApplication.instance() is not None:
QMessageBox().critical(
self.parent(),
"An unexpected error occurred",
"\n".join([message, "", exception]),
QMessageBox.Ok,
)
else:
logging.critical("No QApplication instance available.")

View File

@ -10,7 +10,6 @@ from PySide2.QtCore import (
)
from PySide2.QtGui import QStandardItemModel, QStandardItem, QIcon
from PySide2.QtWidgets import (
QAbstractItemView,
QDialog,
QListView,
QVBoxLayout,
@ -32,38 +31,7 @@ from game.dcs.aircrafttype import AircraftType
from game.squadrons import AirWing, Pilot, Squadron
from game.theater import ControlPoint, ConflictTheater
from gen.flights.flight import FlightType
from qt_ui.models import AirWingModel, SquadronModel
from qt_ui.uiconstants import AIRCRAFT_ICONS
from qt_ui.windows.AirWingDialog import SquadronDelegate
from qt_ui.windows.SquadronDialog import SquadronDialog
class SquadronList(QListView):
"""List view for displaying the air wing's squadrons."""
def __init__(self, air_wing_model: AirWingModel) -> None:
super().__init__()
self.air_wing_model = air_wing_model
self.dialog: Optional[SquadronDialog] = None
self.setIconSize(QSize(91, 24))
self.setItemDelegate(SquadronDelegate(self.air_wing_model))
self.setModel(self.air_wing_model)
self.selectionModel().setCurrentIndex(
self.air_wing_model.index(0, 0, QModelIndex()), QItemSelectionModel.Select
)
# self.setIconSize(QSize(91, 24))
self.setSelectionBehavior(QAbstractItemView.SelectItems)
self.doubleClicked.connect(self.on_double_click)
def on_double_click(self, index: QModelIndex) -> None:
if not index.isValid():
return
self.dialog = SquadronDialog(
SquadronModel(self.air_wing_model.squadron_at_index(index)), self
)
self.dialog.show()
class AllowedMissionTypeControls(QVBoxLayout):

View File

@ -17,6 +17,7 @@ from PySide2.QtWidgets import (
)
from game.squadrons import Squadron
from game.theater import ConflictTheater
from gen.flights.flight import Flight
from qt_ui.delegates import TwoColumnRowDelegate
from qt_ui.models import GameModel, AirWingModel, SquadronModel
@ -56,9 +57,10 @@ class SquadronDelegate(TwoColumnRowDelegate):
class SquadronList(QListView):
"""List view for displaying the air wing's squadrons."""
def __init__(self, air_wing_model: AirWingModel) -> None:
def __init__(self, air_wing_model: AirWingModel, theater: ConflictTheater) -> None:
super().__init__()
self.air_wing_model = air_wing_model
self.theater = theater
self.dialog: Optional[SquadronDialog] = None
self.setIconSize(QSize(91, 24))
@ -76,7 +78,9 @@ class SquadronList(QListView):
if not index.isValid():
return
self.dialog = SquadronDialog(
SquadronModel(self.air_wing_model.squadron_at_index(index)), self
SquadronModel(self.air_wing_model.squadron_at_index(index)),
self.theater,
self,
)
self.dialog.show()
@ -194,7 +198,10 @@ class AirWingTabs(QTabWidget):
def __init__(self, game_model: GameModel) -> None:
super().__init__()
self.addTab(SquadronList(game_model.blue_air_wing_model), "Squadrons")
self.addTab(
SquadronList(game_model.blue_air_wing_model, game_model.game.theater),
"Squadrons",
)
self.addTab(AirInventoryView(game_model), "Inventory")

View File

@ -24,6 +24,7 @@ from qt_ui import liberation_install
from qt_ui.dialogs import Dialog
from qt_ui.models import GameModel
from qt_ui.uiconstants import URLS
from qt_ui.uncaughtexceptionhandler import UncaughtExceptionHandler
from qt_ui.widgets.QTopPanel import QTopPanel
from qt_ui.widgets.ato import QAirTaskingOrderPanel
from qt_ui.widgets.map.QLiberationMap import QLiberationMap
@ -42,7 +43,9 @@ from qt_ui.windows.logs.QLogsWindow import QLogsWindow
class QLiberationWindow(QMainWindow):
def __init__(self, game: Optional[Game]) -> None:
super(QLiberationWindow, self).__init__()
super().__init__()
self._uncaught_exception_handler = UncaughtExceptionHandler(self)
self.game = game
self.game_model = GameModel(game)

View File

@ -1,5 +1,5 @@
import logging
from typing import Callable
from typing import Callable, Iterator, Optional
from PySide2.QtCore import (
QItemSelectionModel,
@ -16,11 +16,14 @@ from PySide2.QtWidgets import (
QHBoxLayout,
QLabel,
QCheckBox,
QComboBox,
)
from game.squadrons import Pilot
from game.squadrons import Pilot, Squadron
from game.theater import ControlPoint, ConflictTheater
from gen.flights.flight import FlightType
from qt_ui.delegates import TwoColumnRowDelegate
from qt_ui.errorreporter import report_errors
from qt_ui.models import SquadronModel
@ -90,10 +93,50 @@ class AutoAssignedTaskControls(QVBoxLayout):
self.squadron_model.set_auto_assignable(task, checked)
class SquadronDestinationComboBox(QComboBox):
def __init__(self, squadron: Squadron, theater: ConflictTheater) -> None:
super().__init__()
self.squadron = squadron
self.theater = theater
room = squadron.location.unclaimed_parking()
self.addItem(
f"Remain at {squadron.location} (room for {room} more aircraft)", None
)
selected_index: Optional[int] = None
for idx, destination in enumerate(sorted(self.iter_destinations(), key=str), 1):
if destination == squadron.destination:
selected_index = idx
room = destination.unclaimed_parking()
self.addItem(
f"Transfer to {destination} (room for {room} more aircraft)",
destination,
)
if squadron.destination is None:
selected_index = 0
if selected_index is not None:
self.setCurrentIndex(selected_index)
def iter_destinations(self) -> Iterator[ControlPoint]:
size = self.squadron.expected_size_next_turn
for control_point in self.theater.control_points_for(self.squadron.player):
if control_point == self:
continue
if not control_point.can_operate(self.squadron.aircraft):
continue
if control_point.unclaimed_parking() < size:
continue
yield control_point
class SquadronDialog(QDialog):
"""Dialog window showing a squadron."""
def __init__(self, squadron_model: SquadronModel, parent) -> None:
def __init__(
self, squadron_model: SquadronModel, theater: ConflictTheater, parent
) -> None:
super().__init__(parent)
self.squadron_model = squadron_model
@ -117,6 +160,15 @@ class SquadronDialog(QDialog):
columns.addWidget(self.pilot_list)
button_panel = QHBoxLayout()
self.transfer_destination = SquadronDestinationComboBox(
squadron_model.squadron, theater
)
self.transfer_destination.currentIndexChanged.connect(
self.on_destination_changed
)
button_panel.addWidget(self.transfer_destination)
button_panel.addStretch()
layout.addLayout(button_panel)
@ -132,6 +184,18 @@ class SquadronDialog(QDialog):
self.toggle_leave_button.clicked.connect(self.toggle_leave)
button_panel.addWidget(self.toggle_leave_button, alignment=Qt.AlignRight)
@property
def squadron(self) -> Squadron:
return self.squadron_model.squadron
def on_destination_changed(self, index: int) -> None:
with report_errors("Could not change squadron destination", self):
destination = self.transfer_destination.itemData(index)
if destination is None:
self.squadron.cancel_relocation()
else:
self.squadron.plan_relocation(destination)
def check_disabled_button_states(
self, button: QPushButton, index: QModelIndex
) -> bool: