From a97fd69828e1028312540c72c59ad5edf65c26bb Mon Sep 17 00:00:00 2001 From: Dan Albert Date: Wed, 31 May 2023 01:09:10 -0700 Subject: [PATCH] Show parking capacities in air wing config. This does show the theoretical parking use of full squadrons even when the new rules are not enabled. Since limits can be enabled manually later in the game, it's still useful information, even if it's a bit misleading. https://github.com/dcs-liberation/dcs_liberation/issues/2910 --- changelog.md | 1 + game/squadrons/squadron.py | 25 +++-- qt_ui/windows/AirWingConfigurationDialog.py | 110 ++++++++++++++++++-- 3 files changed, 115 insertions(+), 21 deletions(-) diff --git a/changelog.md b/changelog.md index a931db38..8c5b186b 100644 --- a/changelog.md +++ b/changelog.md @@ -142,6 +142,7 @@ Saves from 7.0.0 are compatible with 7.1.0 * **[Factions]** Replaced Patriot STRs "EWRs" with AN/FPS-117 for blue factions 1980 or newer. * **[Mission Generation]** Added option to prevent scud and V2 sites from firing at the start of the mission. * **[Mission Planning]** Per-flight TOT offsets can now be set in the flight details UI. This allows individual flights to be scheduled ahead of or behind the rest of the package. +* **[UI]** Parking capacity of each squadron's base is now shown during air wing configuration to avoid overcrowding bases when beginning the game with full squadrons. ## Fixes diff --git a/game/squadrons/squadron.py b/game/squadrons/squadron.py index 6d820781..d627d677 100644 --- a/game/squadrons/squadron.py +++ b/game/squadrons/squadron.py @@ -4,7 +4,8 @@ import logging import random from collections.abc import Iterable from dataclasses import dataclass, field -from typing import Optional, Sequence, TYPE_CHECKING +from typing import Optional, Sequence, TYPE_CHECKING, Any +from uuid import uuid4, UUID from dcs.country import Country from faker import Faker @@ -26,6 +27,8 @@ if TYPE_CHECKING: @dataclass class Squadron: + id: UUID = field(init=False, default_factory=uuid4) + name: str nickname: Optional[str] country: Country @@ -61,21 +64,23 @@ class Squadron: untasked_aircraft: int = field(init=False, hash=False, compare=False, default=0) pending_deliveries: int = field(init=False, hash=False, compare=False, default=0) + def __setstate__(self, state: dict[str, Any]) -> None: + if "id" not in state: + state["id"] = uuid4() + self.__dict__.update(state) + def __str__(self) -> str: if self.nickname is None: return self.name return f'{self.name} "{self.nickname}"' def __hash__(self) -> int: - return hash( - ( - self.name, - self.nickname, - self.country.id, - self.role, - self.aircraft, - ) - ) + return hash(self.id) + + def __eq__(self, other: object) -> bool: + if not isinstance(other, Squadron): + return False + return self.id == other.id @property def player(self) -> bool: diff --git a/qt_ui/windows/AirWingConfigurationDialog.py b/qt_ui/windows/AirWingConfigurationDialog.py index 7a9a29d9..6c0c74c7 100644 --- a/qt_ui/windows/AirWingConfigurationDialog.py +++ b/qt_ui/windows/AirWingConfigurationDialog.py @@ -1,5 +1,6 @@ import logging -from typing import Iterable, Optional, Iterator +from collections import defaultdict +from typing import Iterable, Iterator, Optional from PySide2.QtCore import ( QItemSelection, @@ -190,6 +191,41 @@ class SquadronSizeSpinner(QSpinBox): # return size +class AirWingConfigParkingTracker(QWidget): + allocation_changed = Signal() + + def __init__(self, squadrons: Iterable[Squadron]) -> None: + super().__init__() + self.by_cp: dict[ControlPoint, set[Squadron]] = defaultdict(set) + for squadron in squadrons: + self.add_squadron(squadron) + + def add_squadron(self, squadron: Squadron) -> None: + self.by_cp[squadron.location].add(squadron) + self.signal_change() + + def remove_squadron(self, squadron: Squadron) -> None: + self.by_cp[squadron.location].remove(squadron) + self.signal_change() + + def relocate_squadron( + self, + squadron: Squadron, + prior_location: ControlPoint, + new_location: ControlPoint, + ) -> None: + self.by_cp[prior_location].remove(squadron) + self.by_cp[new_location].add(squadron) + squadron.relocate_to(new_location) + self.signal_change() + + def used_parking_at(self, control_point: ControlPoint) -> int: + return sum(s.max_size for s in self.by_cp[control_point]) + + def signal_change(self) -> None: + self.allocation_changed.emit() + + class SquadronConfigurationBox(QGroupBox): remove_squadron_signal = Signal(Squadron) @@ -198,11 +234,13 @@ class SquadronConfigurationBox(QGroupBox): game: Game, coalition: Coalition, squadron: Squadron, + parking_tracker: AirWingConfigParkingTracker, ) -> None: super().__init__() self.game = game self.coalition = coalition self.squadron = squadron + self.parking_tracker = parking_tracker columns = QHBoxLayout() self.setLayout(columns) @@ -240,6 +278,7 @@ class SquadronConfigurationBox(QGroupBox): task_and_size_row.addLayout(size_column) size_column.addWidget(QLabel("Max size:")) self.max_size_selector = SquadronSizeSpinner(self.squadron.max_size, self) + self.max_size_selector.valueChanged.connect(self.update_max_size) size_column.addWidget(self.max_size_selector) task_column = QVBoxLayout() @@ -254,12 +293,18 @@ class SquadronConfigurationBox(QGroupBox): squadron.location, squadron.aircraft, ) + self.base_selector.currentIndexChanged.connect(self.relocate_squadron) left_column.addWidget(self.base_selector) - if squadron.player and squadron.aircraft.flyable: - player_label = QLabel( - "Players (one per line, leave empty for an AI-only squadron):" - ) + self.parking_label = QLabel() + self.update_parking_label() + self.parking_tracker.allocation_changed.connect(self.update_parking_label) + left_column.addWidget(self.parking_label) + + if not squadron.player and squadron.aircraft.flyable: + player_label = QLabel("Player slots not available for opfor") + elif not squadron.aircraft.flyable: + player_label = QLabel("Player slots not available for non-flyable aircraft") else: msg1 = "Player slots not available for opfor" msg2 = "Player slots not available for non-flyable aircraft" @@ -306,9 +351,26 @@ class SquadronConfigurationBox(QGroupBox): self.player_list.setText( "
".join(p.name for p in self.claim_players_from_squadron()) ) + self.update_parking_label() finally: self.blockSignals(old_state) + def update_parking_label(self) -> None: + self.parking_label.setText( + f"{self.parking_tracker.used_parking_at(self.squadron.location)}/" + f"{self.squadron.location.total_aircraft_parking}" + ) + + def update_max_size(self) -> None: + self.squadron.max_size = self.max_size_selector.value() + self.parking_tracker.signal_change() + + def relocate_squadron(self) -> None: + location = self.base_selector.currentData() + self.parking_tracker.relocate_squadron( + self.squadron, self.squadron.location, location + ) + def remove_from_squadron_config(self) -> None: self.remove_squadron_signal.emit(self.squadron) @@ -361,6 +423,7 @@ class SquadronConfigurationBox(QGroupBox): self.squadron = new_squadron self.bind_data() self.mission_types.replace_squadron(self.squadron) + self.parking_tracker.signal_change() def reset_title(self) -> None: self.setTitle(f"{self.name_edit.text()} - {self.squadron.aircraft}") @@ -400,11 +463,13 @@ class SquadronConfigurationLayout(QVBoxLayout): game: Game, coalition: Coalition, squadrons: list[Squadron], + parking_tracker: AirWingConfigParkingTracker, ) -> None: super().__init__() self.game = game self.coalition = coalition self.squadron_configs = [] + self.parking_tracker = parking_tracker for squadron in squadrons: self.add_squadron(squadron) @@ -415,6 +480,7 @@ class SquadronConfigurationLayout(QVBoxLayout): return keep_squadrons def remove_squadron(self, squadron: Squadron) -> None: + self.parking_tracker.remove_squadron(squadron) for squadron_config in self.squadron_configs: if squadron_config.squadron == squadron: squadron_config.deleteLater() @@ -425,23 +491,32 @@ class SquadronConfigurationLayout(QVBoxLayout): return def add_squadron(self, squadron: Squadron) -> None: - squadron_config = SquadronConfigurationBox(self.game, self.coalition, squadron) + squadron_config = SquadronConfigurationBox( + self.game, self.coalition, squadron, self.parking_tracker + ) squadron_config.remove_squadron_signal.connect(self.remove_squadron) self.squadron_configs.append(squadron_config) self.addWidget(squadron_config) + self.parking_tracker.add_squadron(squadron) class AircraftSquadronsPage(QWidget): remove_squadron_page = Signal(AircraftType) def __init__( - self, game: Game, coalition: Coalition, squadrons: list[Squadron] + self, + game: Game, + coalition: Coalition, + squadrons: list[Squadron], + parking_tracker: AirWingConfigParkingTracker, ) -> None: super().__init__() layout = QVBoxLayout() self.setLayout(layout) - self.squadrons_config = SquadronConfigurationLayout(game, coalition, squadrons) + self.squadrons_config = SquadronConfigurationLayout( + game, coalition, squadrons, parking_tracker + ) self.squadrons_config.config_changed.connect(self.on_squadron_config_changed) scrolling_widget = QWidget() @@ -469,10 +544,16 @@ class AircraftSquadronsPage(QWidget): class AircraftSquadronsPanel(QStackedLayout): page_removed = Signal(AircraftType) - def __init__(self, game: Game, coalition: Coalition) -> None: + def __init__( + self, + game: Game, + coalition: Coalition, + parking_tracker: AirWingConfigParkingTracker, + ) -> None: super().__init__() self.game = game self.coalition = coalition + self.parking_tracker = parking_tracker self.squadrons_pages: dict[AircraftType, AircraftSquadronsPage] = {} for aircraft, squadrons in self.air_wing.squadrons.items(): self.new_page_for_type(aircraft, squadrons) @@ -492,7 +573,9 @@ class AircraftSquadronsPanel(QStackedLayout): def new_page_for_type( self, aircraft_type: AircraftType, squadrons: list[Squadron] ) -> None: - page = AircraftSquadronsPage(self.game, self.coalition, squadrons) + page = AircraftSquadronsPage( + self.game, self.coalition, squadrons, self.parking_tracker + ) page.remove_squadron_page.connect(self.remove_page_for_type) self.addWidget(page) self.squadrons_pages[aircraft_type] = page @@ -587,6 +670,9 @@ class AirWingConfigurationTab(QWidget): self.setLayout(layout) self.game = game self.coalition = coalition + self.parking_tracker = AirWingConfigParkingTracker( + coalition.air_wing.iter_squadrons() + ) self.type_list = AircraftTypeList(coalition.air_wing) @@ -596,7 +682,9 @@ class AirWingConfigurationTab(QWidget): add_button.clicked.connect(lambda state: self.add_squadron()) layout.addWidget(add_button, 2, 1, 1, 1) - self.squadrons_panel = AircraftSquadronsPanel(game, coalition) + self.squadrons_panel = AircraftSquadronsPanel( + game, coalition, self.parking_tracker + ) self.squadrons_panel.page_removed.connect(self.type_list.remove_aircraft_type) layout.addLayout(self.squadrons_panel, 1, 3, 2, 1)