diff --git a/game/squadrons.py b/game/squadrons.py index 62083091..4eb5fbeb 100644 --- a/game/squadrons.py +++ b/game/squadrons.py @@ -1,7 +1,9 @@ from __future__ import annotations import itertools +import logging import random +from collections import defaultdict from dataclasses import dataclass, field from pathlib import Path from typing import Type, Tuple, List, TYPE_CHECKING, Optional, Iterable, Iterator @@ -54,6 +56,9 @@ class Squadron: def __post_init__(self) -> None: self.available_pilots = list(self.active_pilots) + def __str__(self) -> str: + return f'{self.name} "{self.nickname}"' + def claim_available_pilot(self) -> Optional[Pilot]: if not self.available_pilots: self.enlist_new_pilots(1) @@ -100,7 +105,7 @@ class Squadron: from gen.flights.flight import FlightType with path.open() as squadron_file: - data = yaml.load(squadron_file) + data = yaml.safe_load(squadron_file) unit_type = flying_type_from_name(data["aircraft"]) if unit_type is None: @@ -109,7 +114,7 @@ class Squadron: return Squadron( name=data["name"], nickname=data["nickname"], - country=game.country_for(player), + country=data["country"], role=data["role"], aircraft=unit_type, mission_types=tuple(FlightType.from_name(n) for n in data["mission_types"]), @@ -119,19 +124,78 @@ class Squadron: ) +class SquadronLoader: + def __init__(self, game: Game, player: bool) -> None: + self.game = game + self.player = player + + @staticmethod + def squadron_directories() -> Iterator[Path]: + from game import persistency + + yield Path(persistency.base_path()) / "Liberation/Squadrons" + yield Path("resources/squadrons") + + def load(self) -> dict[Type[FlyingType], list[Squadron]]: + squadrons: dict[Type[FlyingType], list[Squadron]] = defaultdict(list) + country = self.game.country_for(self.player) + faction = self.game.faction_for(self.player) + any_country = country.startswith("Combined Joint Task Forces ") + for directory in self.squadron_directories(): + for path, squadron in self.load_squadrons_from(directory): + if not any_country and squadron.country != country: + logging.debug( + "Not using squadron for non-matching country (is " + f"{squadron.country}, need {country}: {path}" + ) + continue + if squadron.aircraft not in faction.aircrafts: + logging.debug( + f"Not using squadron because {faction.name} cannot use " + f"{squadron.aircraft}: {path}" + ) + continue + logging.debug( + f"Found {squadron.name} {squadron.aircraft} {squadron.role} " + f"compatible with {faction.name}" + ) + squadrons[squadron.aircraft].append(squadron) + # Convert away from defaultdict because defaultdict doesn't unpickle so we don't + # want it in the save state. + return dict(squadrons) + + def load_squadrons_from(self, directory: Path) -> Iterator[Tuple[Path, Squadron]]: + logging.debug(f"Looking for factions in {directory}") + # First directory level is the aircraft type so that historical squadrons that + # have flown multiple airframes can be defined as many times as needed. The main + # load() method is responsible for filtering out squadrons that aren't + # compatible with the faction. + for squadron_path in directory.glob("*/*.yaml"): + try: + yield squadron_path, Squadron.from_yaml( + squadron_path, self.game, self.player + ) + except Exception as ex: + raise RuntimeError( + f"Failed to load squadron defined by {squadron_path}" + ) from ex + + class AirWing: def __init__(self, game: Game, player: bool) -> None: from gen.flights.flight import FlightType self.game = game self.player = player - self.squadrons: dict[Type[FlyingType], list[Squadron]] = { - aircraft: [] for aircraft in game.faction_for(player).aircrafts - } - for num, (aircraft, squadrons) in enumerate(self.squadrons.items()): - squadrons.append( + self.squadrons = SquadronLoader(game, player).load() + + count = itertools.count(1) + for aircraft in game.faction_for(player).aircrafts: + if aircraft in self.squadrons: + continue + self.squadrons[aircraft] = [ Squadron( - name=f"Squadron {num + 1:03}", + name=f"Squadron {next(count):03}", nickname=self.random_nickname(), country=game.country_for(player), role="Flying Squadron", @@ -141,7 +205,7 @@ class AirWing: game=game, player=player, ) - ) + ] def squadron_for(self, aircraft: Type[FlyingType]) -> Squadron: return self.squadrons[aircraft][0] diff --git a/gen/flights/flight.py b/gen/flights/flight.py index 7270d329..711e1657 100644 --- a/gen/flights/flight.py +++ b/gen/flights/flight.py @@ -74,9 +74,9 @@ class FlightType(Enum): @classmethod def from_name(cls, name: str) -> FlightType: - for value in cls: - if name == value: - return value + for entry in cls: + if name == entry.value: + return entry raise KeyError(f"No FlightType with name {name}") diff --git a/qt_ui/models.py b/qt_ui/models.py index 51b13e94..df28f034 100644 --- a/qt_ui/models.py +++ b/qt_ui/models.py @@ -397,7 +397,7 @@ class AirWingModel(QAbstractListModel): @staticmethod def text_for_squadron(squadron: Squadron) -> str: """Returns the text that should be displayed for the squadron.""" - return squadron.name + return str(squadron) @staticmethod def icon_for_squadron(squadron: Squadron) -> Optional[QIcon]: diff --git a/qt_ui/windows/SquadronDialog.py b/qt_ui/windows/SquadronDialog.py index 68564544..5aac3f70 100644 --- a/qt_ui/windows/SquadronDialog.py +++ b/qt_ui/windows/SquadronDialog.py @@ -68,7 +68,7 @@ class SquadronDialog(QDialog): self.squadron_model = squadron_model self.setMinimumSize(1000, 440) - self.setWindowTitle(squadron_model.squadron.name) + self.setWindowTitle(str(squadron_model.squadron)) # TODO: self.setWindowIcon() layout = QVBoxLayout() diff --git a/qt_ui/windows/mission/flight/settings/QFlightSlotEditor.py b/qt_ui/windows/mission/flight/settings/QFlightSlotEditor.py index 0223c06f..113e7d8a 100644 --- a/qt_ui/windows/mission/flight/settings/QFlightSlotEditor.py +++ b/qt_ui/windows/mission/flight/settings/QFlightSlotEditor.py @@ -102,7 +102,7 @@ class QFlightSlotEditor(QGroupBox): layout.addWidget(self.aircraft_count_spinner, 0, 1) layout.addWidget(QLabel("Squadron:"), 1, 0) - layout.addWidget(QLabel(self.flight.squadron.name), 1, 1) + layout.addWidget(QLabel(str(self.flight.squadron)), 1, 1) layout.addWidget(QLabel("Assigned pilots:"), 2, 0) self.pilot_selectors = [] diff --git a/resources/squadrons/hornet/VFA-113.yaml b/resources/squadrons/hornet/VFA-113.yaml new file mode 100644 index 00000000..9cee3648 --- /dev/null +++ b/resources/squadrons/hornet/VFA-113.yaml @@ -0,0 +1,21 @@ +--- +name: VFA-113 +nickname: Stingers +country: USA +role: Strike Fighter +aircraft: FA-18C_hornet +mission_types: + - Anti-ship + - BAI + - BARCAP + - CAS + - DEAD + - Escort + - Intercept + - OCA/Aircraft + - OCA/Runway + - SEAD + - SEAD Escort + - Strike + - Fighter sweep + - TARCAP