diff --git a/changelog.md b/changelog.md index 399565ee..36afee9b 100644 --- a/changelog.md +++ b/changelog.md @@ -14,6 +14,8 @@ Saves from 6.x are not compatible with 7.0. * **[Modding]** Add support for VSN F-4B and F-4C mod. * **[Modding]** Custom factions can now be defined in YAML as well as JSON. JSON support may be removed in the future if having both formats causes confusion. * **[Modding]** Campaigns which require custom factions can now define those factions directly in the campaign YAML. See Operation Aliied Sword for an example. +* **[Modding]** The `mission_types` field in squadron files has been removed. Squadron task capability is now determined by airframe, and the auto-assignable list has always been overridden by the campaign settings. +* **[Squadrons]** Squadron-specific mission capability lists no longer restrict players from assigning missions outside the squadron's preferences. ## Fixes diff --git a/game/campaignloader/defaultsquadronassigner.py b/game/campaignloader/defaultsquadronassigner.py index e94e7cfd..8323ce5b 100644 --- a/game/campaignloader/defaultsquadronassigner.py +++ b/game/campaignloader/defaultsquadronassigner.py @@ -115,7 +115,7 @@ class DefaultSquadronAssigner: ) -> bool: if ignore_base_preference: return control_point.can_operate(squadron.aircraft) - return squadron.operates_from(control_point) and task in squadron.mission_types + return squadron.operates_from(control_point) and squadron.capable_of(task) def find_squadron_for_airframe( self, aircraft: AircraftType, task: FlightType, control_point: ControlPoint diff --git a/game/campaignloader/squadrondefgenerator.py b/game/campaignloader/squadrondefgenerator.py index 26e65b8d..f5ff6d70 100644 --- a/game/campaignloader/squadrondefgenerator.py +++ b/game/campaignloader/squadrondefgenerator.py @@ -4,12 +4,12 @@ import itertools import random from typing import Optional, TYPE_CHECKING +from game.ato.ai_flight_planner_db import aircraft_for_task, tasks_for_aircraft from game.ato.flighttype import FlightType from game.dcs.aircrafttype import AircraftType from game.squadrons.operatingbases import OperatingBases from game.squadrons.squadrondef import SquadronDef from game.theater import ControlPoint -from game.ato.ai_flight_planner_db import aircraft_for_task, tasks_for_aircraft if TYPE_CHECKING: from game.factions.faction import Faction @@ -48,7 +48,7 @@ class SquadronDefGenerator: role="Flying Squadron", aircraft=aircraft, livery=None, - mission_types=tuple(tasks_for_aircraft(aircraft)), + auto_assignable_mission_types=set(tasks_for_aircraft(aircraft)), operating_bases=OperatingBases.default_for_aircraft(aircraft), female_pilot_percentage=6, pilot_pool=[], diff --git a/game/squadrons/airwing.py b/game/squadrons/airwing.py index c674b617..19e4a91c 100644 --- a/game/squadrons/airwing.py +++ b/game/squadrons/airwing.py @@ -2,11 +2,11 @@ from __future__ import annotations import itertools from collections import defaultdict -from typing import Sequence, Iterator, TYPE_CHECKING, Optional +from typing import Iterator, Optional, Sequence, TYPE_CHECKING -from game.dcs.aircrafttype import AircraftType from game.ato.ai_flight_planner_db import aircraft_for_task from game.ato.closestairfields import ObjectiveDistanceCache +from game.dcs.aircrafttype import AircraftType from .squadrondefloader import SquadronDefLoader from ..campaignloader.squadrondefgenerator import SquadronDefGenerator from ..factions.faction import Faction @@ -82,7 +82,7 @@ class AirWing: best_aircraft_for_task = aircraft_for_task(task) for aircraft, squadrons in self.squadrons.items(): for squadron in squadrons: - if squadron.untasked_aircraft and task in squadron.mission_types: + if squadron.untasked_aircraft and squadron.capable_of(task): aircrafts.append(aircraft) if aircraft not in best_aircraft_for_task: best_aircraft_for_task.append(aircraft) diff --git a/game/squadrons/squadron.py b/game/squadrons/squadron.py index e6ae98f2..c7eebf5e 100644 --- a/game/squadrons/squadron.py +++ b/game/squadrons/squadron.py @@ -12,6 +12,7 @@ from faker import Faker from game.ato import Flight, FlightType, Package from game.settings import AutoAtoBehavior, Settings from .pilot import Pilot, PilotStatus +from ..ato.ai_flight_planner_db import aircraft_for_task from ..db.database import Database from ..utils import meters @@ -32,7 +33,7 @@ class Squadron: role: str aircraft: AircraftType livery: Optional[str] - mission_types: tuple[FlightType, ...] + auto_assignable_mission_types: set[FlightType] operating_bases: OperatingBases female_pilot_percentage: int @@ -46,10 +47,6 @@ class Squadron: default_factory=list, init=False, hash=False, compare=False ) - auto_assignable_mission_types: set[FlightType] = field( - init=False, hash=False, compare=False - ) - coalition: Coalition = field(hash=False, compare=False) flight_db: Database[Flight] = field(hash=False, compare=False) settings: Settings = field(hash=False, compare=False) @@ -63,9 +60,6 @@ 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 __post_init__(self) -> None: - self.auto_assignable_mission_types = set(self.mission_types) - def __str__(self) -> str: if self.nickname is None: return self.name @@ -94,16 +88,12 @@ class Squadron: def pilot_limits_enabled(self) -> bool: return self.settings.enable_squadron_pilot_limits - def set_allowed_mission_types(self, mission_types: Iterable[FlightType]) -> None: - self.mission_types = tuple(mission_types) - self.auto_assignable_mission_types.intersection_update(self.mission_types) - def set_auto_assignable_mission_types( self, mission_types: Iterable[FlightType] ) -> None: - self.auto_assignable_mission_types = set(self.mission_types).intersection( - mission_types - ) + self.auto_assignable_mission_types = { + t for t in mission_types if self.capable_of(t) + } def claim_new_pilot_if_allowed(self) -> Optional[Pilot]: if self.pilot_limits_enabled: @@ -257,7 +247,20 @@ class Squadron: def has_unfilled_pilot_slots(self) -> bool: return not self.pilot_limits_enabled or self._number_of_unfilled_pilot_slots > 0 + def capable_of(self, task: FlightType) -> bool: + """Returns True if the squadron is capable of performing the given task. + + A squadron may be capable of performing a task even if it will not be + automatically assigned to it. + """ + return self.aircraft in aircraft_for_task(task) + def can_auto_assign(self, task: FlightType) -> bool: + """Returns True if the squadron may be automatically assigned the given task. + + A squadron may be capable of performing a task even if it will not be + automatically assigned to it. + """ return task in self.auto_assignable_mission_types def can_auto_assign_mission( @@ -432,7 +435,7 @@ class Squadron: squadron_def.role, squadron_def.aircraft, squadron_def.livery, - squadron_def.mission_types, + squadron_def.auto_assignable_mission_types, squadron_def.operating_bases, squadron_def.female_pilot_percentage, squadron_def.pilot_pool, diff --git a/game/squadrons/squadrondef.py b/game/squadrons/squadrondef.py index 712dd892..ff535a31 100644 --- a/game/squadrons/squadrondef.py +++ b/game/squadrons/squadrondef.py @@ -1,13 +1,12 @@ from __future__ import annotations -import logging -from collections.abc import Iterable -from dataclasses import dataclass, field +from dataclasses import dataclass from pathlib import Path from typing import Optional, TYPE_CHECKING import yaml +from game.ato.ai_flight_planner_db import aircraft_for_task, tasks_for_aircraft from game.dcs.aircrafttype import AircraftType from game.squadrons.operatingbases import OperatingBases from game.squadrons.pilot import Pilot @@ -25,30 +24,24 @@ class SquadronDef: role: str aircraft: AircraftType livery: Optional[str] - mission_types: tuple[FlightType, ...] + auto_assignable_mission_types: set[FlightType] operating_bases: OperatingBases female_pilot_percentage: int pilot_pool: list[Pilot] claimed: bool = False - auto_assignable_mission_types: set[FlightType] = field( - init=False, hash=False, compare=False - ) - - def __post_init__(self) -> None: - self.auto_assignable_mission_types = set(self.mission_types) - def __str__(self) -> str: if self.nickname is None: return self.name return f'{self.name} "{self.nickname}"' - def set_allowed_mission_types(self, mission_types: Iterable[FlightType]) -> None: - self.mission_types = tuple(mission_types) - self.auto_assignable_mission_types.intersection_update(self.mission_types) + def capable_of(self, task: FlightType) -> bool: + """Returns True if the squadron is capable of performing the given task. - def can_auto_assign(self, task: FlightType) -> bool: - return task in self.auto_assignable_mission_types + A squadron may be capable of performing a task even if it will not be + automatically assigned to it. + """ + return self.aircraft in aircraft_for_task(task) def operates_from(self, control_point: ControlPoint) -> bool: if not control_point.can_operate(self.aircraft): @@ -62,8 +55,6 @@ class SquadronDef: @classmethod def from_yaml(cls, path: Path) -> SquadronDef: - from game.ato.ai_flight_planner_db import tasks_for_aircraft - from game.ato import FlightType with path.open(encoding="utf8") as squadron_file: data = yaml.safe_load(squadron_file) @@ -78,16 +69,6 @@ class SquadronDef: pilots.extend([Pilot(n, player=True) for n in data.get("players", [])]) female_pilot_percentage = data.get("female_pilot_percentage", 6) - mission_types = [FlightType.from_name(n) for n in data["mission_types"]] - tasks = tasks_for_aircraft(unit_type) - for mission_type in list(mission_types): - if mission_type not in tasks: - logging.error( - f"Squadron has mission type {mission_type} but {unit_type} is not " - f"capable of that task: {path}" - ) - mission_types.remove(mission_type) - return SquadronDef( name=data["name"], nickname=data.get("nickname"), @@ -95,7 +76,7 @@ class SquadronDef: role=data["role"], aircraft=unit_type, livery=data.get("livery"), - mission_types=tuple(mission_types), + auto_assignable_mission_types=set(tasks_for_aircraft(unit_type)), operating_bases=OperatingBases.from_yaml(unit_type, data.get("bases", {})), female_pilot_percentage=female_pilot_percentage, pilot_pool=pilots, diff --git a/qt_ui/windows/AirWingConfigurationDialog.py b/qt_ui/windows/AirWingConfigurationDialog.py index 09450870..0ad12c8e 100644 --- a/qt_ui/windows/AirWingConfigurationDialog.py +++ b/qt_ui/windows/AirWingConfigurationDialog.py @@ -44,9 +44,6 @@ class QMissionType: self, mission_type: FlightType, allowed: bool, auto_assignable: bool ) -> None: self.flight_type = mission_type - self.allowed_checkbox = QCheckBox() - self.allowed_checkbox.setChecked(allowed) - self.allowed_checkbox.toggled.connect(self.update_auto_assignable) self.auto_assignable_checkbox = QCheckBox() self.auto_assignable_checkbox.setEnabled(allowed) self.auto_assignable_checkbox.setChecked(auto_assignable) @@ -56,10 +53,6 @@ class QMissionType: if not checked: self.auto_assignable_checkbox.setChecked(False) - @property - def allowed(self) -> bool: - return self.allowed_checkbox.isChecked() - @property def auto_assignable(self) -> bool: return self.auto_assignable_checkbox.isChecked() @@ -72,27 +65,20 @@ class MissionTypeControls(QGridLayout): self.mission_types: list[QMissionType] = [] self.addWidget(QLabel("Mission Type"), 0, 0) - self.addWidget(QLabel("Allow"), 0, 1) - self.addWidget(QLabel("Auto-Assign"), 0, 2) + self.addWidget(QLabel("Auto-Assign"), 0, 1) for i, task in enumerate(FlightType): if task is FlightType.FERRY: # Not plannable so just skip it. continue - allowed = task in squadron.mission_types auto_assignable = task in squadron.auto_assignable_mission_types - mission_type = QMissionType(task, allowed, auto_assignable) + mission_type = QMissionType( + task, squadron.capable_of(task), auto_assignable + ) self.mission_types.append(mission_type) self.addWidget(QLabel(task.value), i + 1, 0) - self.addWidget(mission_type.allowed_checkbox, i + 1, 1) - self.addWidget(mission_type.auto_assignable_checkbox, i + 1, 2) - - @property - def allowed_mission_types(self) -> Iterator[FlightType]: - for mission_type in self.mission_types: - if mission_type.allowed: - yield mission_type.flight_type + self.addWidget(mission_type.auto_assignable_checkbox, i + 1, 1) @property def auto_assignable_mission_types(self) -> Iterator[FlightType]: @@ -233,10 +219,6 @@ class SquadronConfigurationBox(QGroupBox): self.squadron.pilot_pool = [ Pilot(n, player=True) for n in player_names ] + self.squadron.pilot_pool - # Set the allowed mission types - self.squadron.set_allowed_mission_types( - set(self.mission_types.allowed_mission_types) - ) # Also update the auto assignable mission types self.squadron.set_auto_assignable_mission_types( set(self.mission_types.auto_assignable_mission_types) diff --git a/qt_ui/windows/SquadronDialog.py b/qt_ui/windows/SquadronDialog.py index ce3a730e..206660f2 100644 --- a/qt_ui/windows/SquadronDialog.py +++ b/qt_ui/windows/SquadronDialog.py @@ -77,11 +77,12 @@ class AutoAssignedTaskControls(QVBoxLayout): return callback - for task in squadron_model.squadron.mission_types: - checkbox = QCheckBox(text=task.value) - checkbox.setChecked(squadron_model.is_auto_assignable(task)) - checkbox.toggled.connect(make_callback(task)) - self.addWidget(checkbox) + for task in FlightType: + if self.squadron_model.squadron.capable_of(task): + checkbox = QCheckBox(text=task.value) + checkbox.setChecked(squadron_model.is_auto_assignable(task)) + checkbox.toggled.connect(make_callback(task)) + self.addWidget(checkbox) self.addStretch() diff --git a/qt_ui/windows/mission/flight/SquadronSelector.py b/qt_ui/windows/mission/flight/SquadronSelector.py index 03ce2e58..792ba420 100644 --- a/qt_ui/windows/mission/flight/SquadronSelector.py +++ b/qt_ui/windows/mission/flight/SquadronSelector.py @@ -48,7 +48,7 @@ class SquadronSelector(QComboBox): return for squadron in self.air_wing.squadrons_for(aircraft): - if task in squadron.mission_types and squadron.untasked_aircraft: + if squadron.capable_of(task) and squadron.untasked_aircraft: self.addItem(f"{squadron.location}: {squadron}", squadron) if self.count() == 0: