From 485229b92fb6bcdb30e1398a65b8e6a295b8b59b Mon Sep 17 00:00:00 2001 From: Dan Albert Date: Thu, 20 Jul 2023 21:49:21 -0700 Subject: [PATCH] Allow per pilot loadouts and properties. Fixes https://github.com/dcs-liberation/dcs_liberation/issues/3092. --- changelog.md | 1 + game/ato/flight.py | 34 ++++-- game/ato/flightmember.py | 22 ++++ game/ato/flightmembers.py | 89 ++++++++++++++++ game/ato/flightroster.py | 19 ++-- game/ato/iflightroster.py | 34 ++++++ game/ato/loadouts.py | 6 ++ game/data/weapons.py | 6 +- .../aircraft/flightgroupconfigurator.py | 48 +++++---- .../aircraft/waypoints/ocarunwayingress.py | 2 +- .../aircraft/waypoints/seadingress.py | 2 +- game/sim/missionresultsprocessor.py | 2 +- game/unitmap.py | 2 +- qt_ui/blocksignals.py | 13 +++ qt_ui/windows/AirWingDialog.py | 2 +- .../windows/mission/flight/QFlightCreator.py | 7 +- .../windows/mission/flight/QFlightPlanner.py | 5 + .../flight/payload/QFlightPayloadTab.py | 100 ++++++++++++++++-- .../mission/flight/payload/QLoadoutEditor.py | 30 ++++-- .../mission/flight/payload/QPylonEditor.py | 14 ++- .../flight/payload/propertycheckbox.py | 20 +++- .../flight/payload/propertycombobox.py | 22 +++- .../mission/flight/payload/propertyeditor.py | 24 ++++- .../mission/flight/payload/propertyspinbox.py | 20 +++- .../flight/settings/QFlightSlotEditor.py | 50 +++++---- .../settings/QGeneralFlightSettingsTab.py | 8 +- .../flight/waypoints/QFlightWaypointTab.py | 8 +- 27 files changed, 475 insertions(+), 115 deletions(-) create mode 100644 game/ato/flightmember.py create mode 100644 game/ato/flightmembers.py create mode 100644 game/ato/iflightroster.py create mode 100644 qt_ui/blocksignals.py diff --git a/changelog.md b/changelog.md index cbf34e14..d3a438c6 100644 --- a/changelog.md +++ b/changelog.md @@ -209,6 +209,7 @@ BAI/ANTISHIP/DEAD/STRIKE/BARCAP/CAS/OCA/AIR-ASSAULT (main) missions ## Features/Improvements * **[Flight Planning]** Improved IP selection for targets that are near the center of a threat zone. +* **[Flight Planning]** Loadouts and aircraft properties can now be set per-flight member. Warning: AI flights should not use mixed loadouts. * **[Modding]** Factions can now specify the ship type to be used for cargo shipping. The Handy Wind will be used by default, but WW2 factions can pick something more appropriate. * **[UI]** An error will be displayed when invalid fast-forward options are selected rather than beginning a never ending simulation. * **[UI]** Added cheats for instantly repairing and destroying runways. diff --git a/game/ato/flight.py b/game/ato/flight.py index 123f8cc0..3333aafa 100644 --- a/game/ato/flight.py +++ b/game/ato/flight.py @@ -1,6 +1,7 @@ from __future__ import annotations import uuid +from collections.abc import Iterator from datetime import datetime, timedelta from typing import Any, List, Optional, TYPE_CHECKING @@ -9,10 +10,11 @@ from dcs.planes import C_101CC, C_101EB, Su_33, FA_18C_hornet from game.dcs.aircrafttype import AircraftType from pydcs_extensions.hercules.hercules import Hercules +from .flightmembers import FlightMembers from .flightroster import FlightRoster from .flightstate import FlightState, Navigating, Uninitialized from .flightstate.killed import Killed -from .loadouts import Loadout, Weapon +from .loadouts import Weapon from ..radio.RadioFrequencyContainer import RadioFrequencyContainer from ..radio.TacanContainer import TacanContainer from ..radio.radios import RadioFrequency @@ -31,6 +33,8 @@ if TYPE_CHECKING: from game.squadrons import Squadron, Pilot from game.theater import ControlPoint from game.transfers import TransferOrder + from game.data.weapons import WeaponType + from .flightmember import FlightMember from .flightplans.flightplan import FlightPlan from .flighttype import FlightType from .flightwaypoint import FlightWaypoint @@ -61,21 +65,16 @@ class Flight(SidcDescribable, RadioFrequencyContainer, TacanContainer): self.package = package self.coalition = squadron.coalition self.squadron = squadron + self.flight_type = flight_type if claim_inv: self.squadron.claim_inventory(count) if roster is None: - self.roster = FlightRoster(self.squadron, initial_size=count) + self.roster = FlightMembers(self, initial_size=count) else: - self.roster = roster + self.roster = FlightMembers.from_roster(self, roster) self.divert = divert - self.flight_type = flight_type - self.loadout = Loadout.default_for(self) - if self.squadron.aircraft.name == "F-15I Ra'am": - self.loadout.pylons[16] = Weapon.with_clsid( - "{IDF_MODS_PROJECT_F-15I_Raam_Dome}" - ) + self.start_type = start_type - self.use_custom_loadout = False self.custom_name = custom_name self.group_id: int = 0 @@ -85,6 +84,7 @@ class Flight(SidcDescribable, RadioFrequencyContainer, TacanContainer): self.tcn_name = callsign self.initialize_fuel() + self.use_same_loadout_for_all_members = True # Only used by transport missions. self.cargo = cargo @@ -138,7 +138,11 @@ class Flight(SidcDescribable, RadioFrequencyContainer, TacanContainer): def __setstate__(self, state: dict[str, Any]) -> None: state["state"] = Uninitialized(self, state["squadron"].settings) + if "use_same_loadout_for_all_members" not in state: + state["use_same_loadout_for_all_members"] = True self.__dict__.update(state) + if isinstance(self.roster, FlightRoster): + self.roster = FlightMembers.from_roster(self, self.roster) @property def blue(self) -> bool: @@ -206,6 +210,9 @@ class Flight(SidcDescribable, RadioFrequencyContainer, TacanContainer): def missing_pilots(self) -> int: return self.roster.missing_pilots + def iter_members(self) -> Iterator[FlightMember]: + yield from self.roster.members + def set_flight_type(self, var: FlightType) -> None: self.flight_type = var @@ -235,6 +242,11 @@ class Flight(SidcDescribable, RadioFrequencyContainer, TacanContainer): elif self.departure.cptype.name in ["FARP", "FOB"] and not self.is_helo: self.fuel = unit_type.fuel_max * 0.75 + def any_member_has_weapon_of_type(self, weapon_type: WeaponType) -> bool: + return any( + m.loadout.has_weapon_of_type(weapon_type) for m in self.iter_members() + ) + def __repr__(self) -> str: return self.__str__() @@ -294,7 +306,7 @@ class Flight(SidcDescribable, RadioFrequencyContainer, TacanContainer): Killed(self.state.estimate_position(), self, self.squadron.settings) ) events.update_flight(self) - for pilot in self.roster.pilots: + for pilot in self.roster.iter_pilots(): if pilot is not None: results.kill_pilot(self, pilot) diff --git a/game/ato/flightmember.py b/game/ato/flightmember.py new file mode 100644 index 00000000..7a9a3410 --- /dev/null +++ b/game/ato/flightmember.py @@ -0,0 +1,22 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +from game.ato.loadouts import Loadout + +if TYPE_CHECKING: + from game.squadrons import Pilot + + +class FlightMember: + def __init__(self, pilot: Pilot | None, loadout: Loadout) -> None: + self.pilot = pilot + self.loadout = loadout + self.use_custom_loadout = False + self.properties: dict[str, bool | float | int] = {} + + @property + def is_player(self) -> bool: + if self.pilot is None: + return False + return self.pilot.player diff --git a/game/ato/flightmembers.py b/game/ato/flightmembers.py new file mode 100644 index 00000000..028106f9 --- /dev/null +++ b/game/ato/flightmembers.py @@ -0,0 +1,89 @@ +from __future__ import annotations + +from collections.abc import Iterator +from typing import Optional, TYPE_CHECKING + +from .flightmember import FlightMember +from .flightroster import FlightRoster +from .iflightroster import IFlightRoster +from .loadouts import Loadout +from ..data.weapons import Weapon + +if TYPE_CHECKING: + from game.squadrons import Pilot + from .flight import Flight + + +class FlightMembers(IFlightRoster): + def __init__(self, flight: Flight, initial_size: int = 0) -> None: + self.flight = flight + self.members: list[FlightMember] = [] + self.resize(initial_size) + + @staticmethod + def from_roster(flight: Flight, roster: FlightRoster) -> FlightMembers: + members = FlightMembers(flight) + loadout = Loadout.default_for(flight) + if flight.squadron.aircraft.name == "F-15I Ra'am": + loadout.pylons[16] = Weapon.with_clsid( + "{IDF_MODS_PROJECT_F-15I_Raam_Dome}" + ) + members.members = [FlightMember(p, loadout) for p in roster.pilots] + return members + + def iter_pilots(self) -> Iterator[Pilot | None]: + yield from (m.pilot for m in self.members) + + def pilot_at(self, idx: int) -> Pilot | None: + return self.members[idx].pilot + + @property + def max_size(self) -> int: + return len(self.members) + + @property + def player_count(self) -> int: + return len([m for m in self.members if m.is_player]) + + @property + def missing_pilots(self) -> int: + return len([m for m in self.members if m.pilot is None]) + + def resize(self, new_size: int) -> None: + if self.max_size > new_size: + self.flight.squadron.return_pilots( + [m.pilot for m in self.members[new_size:] if m.pilot is not None] + ) + self.members = self.members[:new_size] + return + if self.max_size: + loadout = self.members[0].loadout.clone() + else: + loadout = Loadout.default_for(self.flight) + if self.flight.squadron.aircraft.name == "F-15I Ra'am": + loadout.pylons[16] = Weapon.with_clsid( + "{IDF_MODS_PROJECT_F-15I_Raam_Dome}" + ) + for _ in range(new_size - self.max_size): + member = FlightMember(self.flight.squadron.claim_available_pilot(), loadout) + member.use_custom_loadout = loadout.is_custom + self.members.append(member) + + def set_pilot(self, index: int, pilot: Optional[Pilot]) -> None: + if pilot is not None: + self.flight.squadron.claim_pilot(pilot) + if (current_pilot := self.pilot_at(index)) is not None: + self.flight.squadron.return_pilot(current_pilot) + self.members[index].pilot = pilot + + def clear(self) -> None: + self.flight.squadron.return_pilots( + [p for p in self.iter_pilots() if p is not None] + ) + + def use_same_loadout_for_all_members(self) -> None: + if not self.members: + return + loadout = self.members[0].loadout + for member in self.members[1:]: + member.loadout = loadout.clone() diff --git a/game/ato/flightroster.py b/game/ato/flightroster.py index 6f7b6326..a9ef5016 100644 --- a/game/ato/flightroster.py +++ b/game/ato/flightroster.py @@ -1,29 +1,30 @@ from __future__ import annotations +from collections.abc import Iterator from typing import Optional, TYPE_CHECKING +from game.ato.iflightroster import IFlightRoster + if TYPE_CHECKING: from game.squadrons import Squadron, Pilot -class FlightRoster: +class FlightRoster(IFlightRoster): def __init__(self, squadron: Squadron, initial_size: int = 0) -> None: self.squadron = squadron self.pilots: list[Optional[Pilot]] = [] self.resize(initial_size) + def iter_pilots(self) -> Iterator[Pilot | None]: + yield from self.pilots + + def pilot_at(self, idx: int) -> Pilot | None: + return self.pilots[idx] + @property def max_size(self) -> int: return len(self.pilots) - @property - def player_count(self) -> int: - return len([p for p in self.pilots if p is not None and p.player]) - - @property - def missing_pilots(self) -> int: - return len([p for p in self.pilots if p is None]) - def resize(self, new_size: int) -> None: if self.max_size > new_size: self.squadron.return_pilots( diff --git a/game/ato/iflightroster.py b/game/ato/iflightroster.py new file mode 100644 index 00000000..32231445 --- /dev/null +++ b/game/ato/iflightroster.py @@ -0,0 +1,34 @@ +from __future__ import annotations + +from abc import ABC, abstractmethod +from typing import Optional, TYPE_CHECKING, Iterator + +if TYPE_CHECKING: + from game.squadrons import Pilot + + +class IFlightRoster(ABC): + @abstractmethod + def iter_pilots(self) -> Iterator[Pilot | None]: + ... + + @abstractmethod + def pilot_at(self, idx: int) -> Pilot | None: + ... + + @property + @abstractmethod + def max_size(self) -> int: + ... + + @abstractmethod + def resize(self, new_size: int) -> None: + ... + + @abstractmethod + def set_pilot(self, index: int, pilot: Optional[Pilot]) -> None: + ... + + @abstractmethod + def clear(self) -> None: + ... diff --git a/game/ato/loadouts.py b/game/ato/loadouts.py index d8a04d1c..b1478f05 100644 --- a/game/ato/loadouts.py +++ b/game/ato/loadouts.py @@ -1,5 +1,6 @@ from __future__ import annotations +import copy import datetime import logging from collections.abc import Iterable @@ -35,6 +36,11 @@ class Loadout: def derive_custom(self, name: str) -> Loadout: return Loadout(name, self.pylons, self.date, is_custom=True) + def clone(self) -> Loadout: + return Loadout( + self.name, dict(self.pylons), copy.deepcopy(self.date), self.is_custom + ) + def has_weapon_of_type(self, weapon_type: WeaponType) -> bool: for weapon in self.pylons.values(): if weapon is not None and weapon.weapon_group.type is weapon_type: diff --git a/game/data/weapons.py b/game/data/weapons.py index 5cd35c99..7072f90e 100644 --- a/game/data/weapons.py +++ b/game/data/weapons.py @@ -10,7 +10,7 @@ from pathlib import Path from typing import Iterator, Optional, Any, ClassVar import yaml -from dcs.unitgroup import FlyingGroup +from dcs.flyingunit import FlyingUnit from dcs.weapons_data import weapon_ids from game.dcs.aircrafttype import AircraftType @@ -278,10 +278,10 @@ class Pylon: # configuration. return weapon in self.allowed or weapon.clsid == "" - def equip(self, group: FlyingGroup[Any], weapon: Weapon) -> None: + def equip(self, unit: FlyingUnit, weapon: Weapon) -> None: if not self.can_equip(weapon): logging.error(f"Pylon {self.number} cannot equip {weapon.name}") - group.load_pylon(self.make_pydcs_assignment(weapon), self.number) + unit.load_pylon(self.make_pydcs_assignment(weapon), self.number) def make_pydcs_assignment(self, weapon: Weapon) -> PydcsWeaponAssignment: return self.number, weapon.pydcs_data diff --git a/game/missiongenerator/aircraft/flightgroupconfigurator.py b/game/missiongenerator/aircraft/flightgroupconfigurator.py index 10e1cea7..9814eaae 100644 --- a/game/missiongenerator/aircraft/flightgroupconfigurator.py +++ b/game/missiongenerator/aircraft/flightgroupconfigurator.py @@ -27,6 +27,7 @@ from .aircraftbehavior import AircraftBehavior from .aircraftpainter import AircraftPainter from .flightdata import FlightData from .waypoints import WaypointGenerator +from ...ato.flightmember import FlightMember from ...ato.flightplans.aewc import AewcFlightPlan from ...ato.flightplans.packagerefueling import PackageRefuelingFlightPlan from ...ato.flightplans.theaterrefueling import TheaterRefuelingFlightPlan @@ -67,13 +68,13 @@ class FlightGroupConfigurator: AircraftBehavior(self.flight.flight_type).apply_to(self.flight, self.group) AircraftPainter(self.flight, self.group).apply_livery() self.setup_props() - self.setup_payload() + self.setup_payloads() self.setup_fuel() flight_channel = self.setup_radios() laser_codes: list[Optional[int]] = [] - for unit, pilot in zip(self.group.units, self.flight.roster.pilots): - self.configure_flight_member(unit, pilot, laser_codes) + for unit, member in zip(self.group.units, self.flight.iter_members()): + self.configure_flight_member(unit, member, laser_codes) divert = None if self.flight.divert is not None: @@ -143,21 +144,20 @@ class FlightGroupConfigurator: ) def configure_flight_member( - self, unit: FlyingUnit, pilot: Optional[Pilot], laser_codes: list[Optional[int]] + self, unit: FlyingUnit, member: FlightMember, laser_codes: list[Optional[int]] ) -> None: - player = pilot is not None and pilot.player - self.set_skill(unit, pilot) - if self.flight.loadout.has_weapon_of_type(WeaponTypeEnum.TGP) and player: + self.set_skill(unit, member) + if member.loadout.has_weapon_of_type(WeaponTypeEnum.TGP) and member.is_player: laser_codes.append(self.laser_code_registry.get_next_laser_code()) else: laser_codes.append(None) settings = self.flight.coalition.game.settings - if not player or not settings.plugins.get("ewrj"): + if not member.is_player or not settings.plugins.get("ewrj"): return jammer_required = settings.plugin_option("ewrj.ecm_required") if jammer_required: ecm = WeaponTypeEnum.JAMMER - if not self.flight.loadout.has_weapon_of_type(ecm): + if not member.loadout.has_weapon_of_type(ecm): return ewrj_menu_trigger = TriggerStart(comment=f"EWRJ-{unit.name}") ewrj_menu_trigger.add_action(DoScript(String(f'EWJamming("{unit.name}")'))) @@ -221,9 +221,9 @@ class FlightGroupConfigurator: ) ) - def set_skill(self, unit: FlyingUnit, pilot: Optional[Pilot]) -> None: - if pilot is None or not pilot.player: - unit.skill = self.skill_level_for(unit, pilot) + def set_skill(self, unit: FlyingUnit, member: FlightMember) -> None: + if not member.is_player: + unit.skill = self.skill_level_for(unit, member.pilot) return if self.use_client or "Pilot #1" not in unit.name: @@ -260,15 +260,18 @@ class FlightGroupConfigurator: return levels[new_level] def setup_props(self) -> None: - for prop_id, value in self.flight.props.items(): - for unit in self.group.units: + for unit, member in zip(self.group.units, self.flight.iter_members()): + for prop_id, value in member.properties.items(): unit.set_property(prop_id, value) - def setup_payload(self) -> None: - for p in self.group.units: - p.pylons.clear() + def setup_payloads(self) -> None: + for unit, member in zip(self.group.units, self.flight.iter_members()): + self.setup_payload(unit, member) - loadout = self.flight.loadout + def setup_payload(self, unit: FlyingUnit, member: FlightMember) -> None: + unit.pylons.clear() + + loadout = member.loadout if self.game.settings.restrict_weapons_by_date: loadout = loadout.degrade_for_date(self.flight.unit_type, self.game.date) @@ -276,7 +279,7 @@ class FlightGroupConfigurator: if weapon is None: continue pylon = Pylon.for_aircraft(self.flight.unit_type, pylon_number) - pylon.equip(self.group, weapon) + pylon.equip(unit, weapon) def setup_fuel(self) -> None: fuel = self.flight.state.estimate_fuel() @@ -287,5 +290,8 @@ class FlightGroupConfigurator: "starting fuel to 100kg." ) fuel = 100 - for unit, pilot in zip(self.group.units, self.flight.roster.pilots): - unit.fuel = fuel + for unit, pilot in zip(self.group.units, self.flight.roster.iter_pilots()): + if pilot is not None and pilot.player: + unit.fuel = fuel + else: + unit.fuel = self.flight.fuel diff --git a/game/missiongenerator/aircraft/waypoints/ocarunwayingress.py b/game/missiongenerator/aircraft/waypoints/ocarunwayingress.py index f26ed1fe..66458877 100644 --- a/game/missiongenerator/aircraft/waypoints/ocarunwayingress.py +++ b/game/missiongenerator/aircraft/waypoints/ocarunwayingress.py @@ -29,7 +29,7 @@ class OcaRunwayIngressBuilder(PydcsWaypointBuilder): # for more details. # The LGB work around assumes the Airfield position in DCS is on a runway, which seems # to be the case for most if not all airfields. - if self.flight.loadout.has_weapon_of_type(WeaponType.LGB): + if self.flight.any_member_has_weapon_of_type(WeaponType.LGB): waypoint.tasks.append( Bombing( position=target.position, diff --git a/game/missiongenerator/aircraft/waypoints/seadingress.py b/game/missiongenerator/aircraft/waypoints/seadingress.py index 48503011..f4762b16 100644 --- a/game/missiongenerator/aircraft/waypoints/seadingress.py +++ b/game/missiongenerator/aircraft/waypoints/seadingress.py @@ -53,7 +53,7 @@ class SeadIngressBuilder(PydcsWaypointBuilder): ) waypoint.tasks.append(attack_task) - if self.flight.loadout.has_weapon_of_type(WeaponType.ARM): + if self.flight.any_member_has_weapon_of_type(WeaponType.ARM): # Special handling for ARM Weapon types: # The SEAD flight will Search for the targeted group and then engage it # if it is found only. This will prevent AI from having huge problems diff --git a/game/sim/missionresultsprocessor.py b/game/sim/missionresultsprocessor.py index b3f03a25..4fc3f808 100644 --- a/game/sim/missionresultsprocessor.py +++ b/game/sim/missionresultsprocessor.py @@ -60,7 +60,7 @@ class MissionResultsProcessor: def _commit_pilot_experience(ato: AirTaskingOrder) -> None: for package in ato.packages: for flight in package.flights: - for idx, pilot in enumerate(flight.roster.pilots): + for idx, pilot in enumerate(flight.roster.iter_pilots()): if pilot is None: logging.error( f"Cannot award experience to pilot #{idx} of {flight} " diff --git a/game/unitmap.py b/game/unitmap.py index 066536ff..2744ca7f 100644 --- a/game/unitmap.py +++ b/game/unitmap.py @@ -68,7 +68,7 @@ class UnitMap: self.airlifts: Dict[str, AirliftUnits] = {} def add_aircraft(self, group: FlyingGroup[Any], flight: Flight) -> None: - for pilot, unit in zip(flight.roster.pilots, group.units): + for pilot, unit in zip(flight.roster.iter_pilots(), group.units): # The actual name is a String (the pydcs translatable string), which # doesn't define __eq__. name = str(unit.name) diff --git a/qt_ui/blocksignals.py b/qt_ui/blocksignals.py new file mode 100644 index 00000000..8019946a --- /dev/null +++ b/qt_ui/blocksignals.py @@ -0,0 +1,13 @@ +from collections.abc import Iterator +from contextlib import contextmanager + +from PySide6.QtWidgets import QWidget + + +@contextmanager +def block_signals(widget: QWidget) -> Iterator[None]: + blocked = widget.blockSignals(True) + try: + yield + finally: + widget.blockSignals(blocked) diff --git a/qt_ui/windows/AirWingDialog.py b/qt_ui/windows/AirWingDialog.py index e31130b4..41ab20df 100644 --- a/qt_ui/windows/AirWingDialog.py +++ b/qt_ui/windows/AirWingDialog.py @@ -121,7 +121,7 @@ class AircraftInventoryData: flight_type = flight.flight_type.value target = flight.package.target.name for idx in range(0, num_units): - pilot = flight.roster.pilots[idx] + pilot = flight.roster.pilot_at(idx) if pilot is None: pilot_name = "Unassigned" player = "" diff --git a/qt_ui/windows/mission/flight/QFlightCreator.py b/qt_ui/windows/mission/flight/QFlightCreator.py index 880979fa..e0ffc86c 100644 --- a/qt_ui/windows/mission/flight/QFlightCreator.py +++ b/qt_ui/windows/mission/flight/QFlightCreator.py @@ -89,9 +89,10 @@ class QFlightCreator(QDialog): roster = None else: roster = FlightRoster( - squadron, initial_size=self.flight_size_spinner.value() + squadron, + initial_size=self.flight_size_spinner.value(), ) - self.roster_editor = FlightRosterEditor(roster) + self.roster_editor = FlightRosterEditor(squadron, roster) self.flight_size_spinner.valueChanged.connect(self.roster_editor.resize) self.squadron_selector.currentIndexChanged.connect(self.on_squadron_changed) roster_layout = QHBoxLayout() @@ -232,7 +233,7 @@ class QFlightCreator(QDialog): self.roster_editor.replace(None) if squadron is not None: self.roster_editor.replace( - FlightRoster(squadron, self.flight_size_spinner.value()) + squadron, FlightRoster(squadron, self.flight_size_spinner.value()) ) self.on_departure_changed(squadron.location) diff --git a/qt_ui/windows/mission/flight/QFlightPlanner.py b/qt_ui/windows/mission/flight/QFlightPlanner.py index e6d0a2d2..19a868e5 100644 --- a/qt_ui/windows/mission/flight/QFlightPlanner.py +++ b/qt_ui/windows/mission/flight/QFlightPlanner.py @@ -19,6 +19,11 @@ class QFlightPlanner(QTabWidget): self.general_settings_tab = QGeneralFlightSettingsTab( gm, package_model, flight, self.waypoint_tab.flight_waypoint_list ) + self.payload_tab = QFlightPayloadTab(flight, gm.game) + self.general_settings_tab.flight_size_changed.connect( + self.payload_tab.resize_for_flight + ) + self.waypoint_tab = QFlightWaypointTab(gm.game, package_model.package, flight) self.waypoint_tab.loadout_changed.connect(self.payload_tab.reload_from_flight) self.addTab(self.general_settings_tab, "General Flight settings") self.addTab(self.payload_tab, "Payload") diff --git a/qt_ui/windows/mission/flight/payload/QFlightPayloadTab.py b/qt_ui/windows/mission/flight/payload/QFlightPayloadTab.py index 83b68f3a..1a84790e 100644 --- a/qt_ui/windows/mission/flight/payload/QFlightPayloadTab.py +++ b/qt_ui/windows/mission/flight/payload/QFlightPayloadTab.py @@ -9,26 +9,41 @@ from PySide2.QtWidgets import ( QWidget, QSpinBox, QSlider, + QCheckBox, ) from game import Game from game.ato.flight import Flight +from game.ato.flightmember import FlightMember from game.ato.loadouts import Loadout +from qt_ui.widgets.QLabeledWidget import QLabeledWidget from .QLoadoutEditor import QLoadoutEditor from .propertyeditor import PropertyEditor class DcsLoadoutSelector(QComboBox): - def __init__(self, flight: Flight) -> None: + def __init__(self, flight: Flight, member: FlightMember) -> None: super().__init__() for loadout in Loadout.iter_for(flight): self.addItem(loadout.name, loadout) self.model().sort(0) - self.setDisabled(flight.loadout.is_custom) - if flight.loadout.is_custom: + self.setDisabled(member.loadout.is_custom) + if member.loadout.is_custom: self.setCurrentText(Loadout.default_for(flight).name) else: - self.setCurrentText(flight.loadout.name) + self.setCurrentText(member.loadout.name) + + +class FlightMemberSelector(QSpinBox): + def __init__(self, flight: Flight, parent: QWidget | None = None) -> None: + super().__init__(parent) + self.flight = flight + self.setMinimum(0) + self.setMaximum(flight.count - 1) + + @property + def selected_member(self) -> FlightMember: + return self.flight.roster.members[self.value()] class DcsFuelSelector(QHBoxLayout): @@ -99,12 +114,41 @@ class QFlightPayloadTab(QFrame): def __init__(self, flight: Flight, game: Game): super(QFlightPayloadTab, self).__init__() self.flight = flight - self.payload_editor = QLoadoutEditor(flight, game) + self.payload_editor = QLoadoutEditor( + flight, self.flight.roster.members[0], game + ) self.payload_editor.toggled.connect(self.on_custom_toggled) self.payload_editor.saved.connect(self.on_saved_payload) layout = QVBoxLayout() + self.member_selector = FlightMemberSelector(self.flight, self) + self.member_selector.valueChanged.connect(self.rebind_to_selected_member) + layout.addLayout(QLabeledWidget("Flight member:", self.member_selector)) + self.same_loadout_for_all_checkbox = QCheckBox( + "Use same loadout for all flight members" + ) + self.same_loadout_for_all_checkbox.setChecked( + self.flight.use_same_loadout_for_all_members + ) + self.same_loadout_for_all_checkbox.toggled.connect(self.on_same_loadout_toggled) + layout.addWidget(self.same_loadout_for_all_checkbox) + layout.addWidget( + QLabel( + "Warning: AI flights should use the same loadout for all members." + ) + ) + + scroll_content = QWidget() + scrolling_layout = QVBoxLayout() + scroll_content.setLayout(scrolling_layout) + + scroll = QScrollArea() + scroll.setWidgetResizable(True) + scroll.setWidget(scroll_content) + scroll.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) + layout.addWidget(scroll) + # Docs Link docsText = QLabel( 'How to create your own default loadout' @@ -122,6 +166,13 @@ class QFlightPayloadTab(QFrame): layout.addLayout(self.fuel_selector) self.loadout_selector = DcsLoadoutSelector(flight) + self.property_editor = PropertyEditor( + self.flight, self.member_selector.selected_member + ) + scrolling_layout.addLayout(self.property_editor) + self.loadout_selector = DcsLoadoutSelector( + flight, self.member_selector.selected_member + ) self.loadout_selector.currentIndexChanged.connect(self.on_new_loadout) layout.addWidget(self.loadout_selector) layout.addWidget(self.payload_editor, stretch=1) @@ -129,8 +180,27 @@ class QFlightPayloadTab(QFrame): self.setLayout(layout) + def resize_for_flight(self) -> None: + self.member_selector.setMaximum(self.flight.count - 1) + def reload_from_flight(self) -> None: - self.loadout_selector.setCurrentText(self.flight.loadout.name) + self.loadout_selector.setCurrentText( + self.member_selector.selected_member.loadout.name + ) + + def rebind_to_selected_member(self) -> None: + member = self.member_selector.selected_member + self.property_editor.set_flight_member(member) + self.loadout_selector.setCurrentText(member.loadout.name) + self.loadout_selector.setDisabled(member.loadout.is_custom) + self.payload_editor.set_flight_member(member) + if self.member_selector.value() != 0: + self.loadout_selector.setDisabled( + self.flight.use_same_loadout_for_all_members + ) + self.payload_editor.setDisabled( + self.flight.use_same_loadout_for_all_members + ) def loadout_at(self, index: int) -> Loadout: loadout = self.loadout_selector.itemData(index) @@ -145,18 +215,30 @@ class QFlightPayloadTab(QFrame): return loadout def on_new_loadout(self, index: int) -> None: - self.flight.loadout = self.loadout_at(index) + self.member_selector.selected_member.loadout = self.loadout_at(index) self.payload_editor.reset_pylons() def on_custom_toggled(self, use_custom: bool) -> None: self.loadout_selector.setDisabled(use_custom) + member = self.member_selector.selected_member + member.use_custom_loadout = use_custom if use_custom: - self.flight.loadout = self.flight.loadout.derive_custom("Custom") + member.loadout = member.loadout.derive_custom("Custom") else: - self.flight.loadout = self.current_loadout() + member.loadout = self.current_loadout() self.payload_editor.reset_pylons() def on_saved_payload(self, payload_name: str) -> None: loadout = self.flight.loadout self.loadout_selector.addItem(payload_name, loadout) self.loadout_selector.setCurrentIndex(self.loadout_selector.count() - 1) + + def on_same_loadout_toggled(self, checked: bool) -> None: + self.flight.use_same_loadout_for_all_members = checked + if self.member_selector.value(): + self.loadout_selector.setDisabled(checked) + self.payload_editor.setDisabled(checked) + if checked: + self.flight.roster.use_same_loadout_for_all_members() + if self.member_selector.value(): + self.rebind_to_selected_member() diff --git a/qt_ui/windows/mission/flight/payload/QLoadoutEditor.py b/qt_ui/windows/mission/flight/payload/QLoadoutEditor.py index a5a618e3..7b9367a4 100644 --- a/qt_ui/windows/mission/flight/payload/QLoadoutEditor.py +++ b/qt_ui/windows/mission/flight/payload/QLoadoutEditor.py @@ -1,3 +1,4 @@ +from collections.abc import Iterator from dataclasses import dataclass from shutil import copyfile from typing import Dict, Union @@ -18,20 +19,23 @@ from dcs import lua from game import Game from game.ato.flight import Flight +from game.ato.flightmember import FlightMember from game.data.weapons import Pylon from game.persistency import payloads_dir +from qt_ui.blocksignals import block_signals from qt_ui.windows.mission.flight.payload.QPylonEditor import QPylonEditor class QLoadoutEditor(QGroupBox): saved = Signal(str) - def __init__(self, flight: Flight, game: Game) -> None: + def __init__(self, flight: Flight, flight_member: FlightMember, game: Game) -> None: super().__init__("Use custom loadout") self.flight = flight + self.flight_member = flight_member self.game = game self.setCheckable(True) - self.setChecked(flight.loadout.is_custom) + self.setChecked(flight_member.loadout.is_custom) vbox = QVBoxLayout(self) layout = QGridLayout(self) @@ -40,7 +44,7 @@ class QLoadoutEditor(QGroupBox): label = QLabel(f"{pylon.number}") label.setSizePolicy(QSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)) layout.addWidget(label, i, 0) - layout.addWidget(QPylonEditor(game, flight, pylon), i, 1) + layout.addWidget(QPylonEditor(game, flight, flight_member, pylon), i, 1) vbox.addLayout(layout) @@ -60,8 +64,18 @@ class QLoadoutEditor(QGroupBox): self.setLayout(vbox) - for i in self.findChildren(QPylonEditor): - i.set_from(self.flight.loadout) + for pylon_editor in self.iter_pylon_editors(): + pylon_editor.set_from(self.flight_member.loadout) + + def iter_pylon_editors(self) -> Iterator[QPylonEditor]: + yield from self.findChildren(QPylonEditor) + + def set_flight_member(self, flight_member: FlightMember) -> None: + self.flight_member = flight_member + with block_signals(self): + self.setChecked(self.flight_member.use_custom_loadout) + for pylon_editor in self.iter_pylon_editors(): + pylon_editor.set_flight_member(flight_member) def _backup_payloads(self) -> None: ac_id = self.flight.unit_type.dcs_unit_type.id @@ -146,10 +160,10 @@ class QLoadoutEditor(QGroupBox): return payload_name_input def reset_pylons(self) -> None: - self.flight.use_custom_loadout = self.isChecked() + self.flight_member.use_custom_loadout = self.isChecked() if not self.isChecked(): - for i in self.findChildren(QPylonEditor): - i.set_from(self.flight.loadout) + for pylon_editor in self.iter_pylon_editors(): + pylon_editor.set_from(self.flight_member.loadout) @dataclass diff --git a/qt_ui/windows/mission/flight/payload/QPylonEditor.py b/qt_ui/windows/mission/flight/payload/QPylonEditor.py index 057e12bf..b548f85d 100644 --- a/qt_ui/windows/mission/flight/payload/QPylonEditor.py +++ b/qt_ui/windows/mission/flight/payload/QPylonEditor.py @@ -6,19 +6,23 @@ from PySide2.QtWidgets import QComboBox from game import Game from game.ato.flight import Flight +from game.ato.flightmember import FlightMember from game.ato.loadouts import Loadout from game.data.weapons import Pylon, Weapon class QPylonEditor(QComboBox): - def __init__(self, game: Game, flight: Flight, pylon: Pylon) -> None: + def __init__( + self, game: Game, flight: Flight, flight_member: FlightMember, pylon: Pylon + ) -> None: super().__init__() self.flight = flight + self.flight_member = flight_member self.pylon = pylon self.game = game self.has_added_clean_item = False - current = self.flight.loadout.pylons.get(self.pylon.number) + current = self.flight_member.loadout.pylons.get(self.pylon.number) self.addItem("None", None) if self.game.settings.restrict_weapons_by_date: @@ -35,7 +39,7 @@ class QPylonEditor(QComboBox): def on_pylon_change(self): selected: Optional[Weapon] = self.currentData() - self.flight.loadout.pylons[self.pylon.number] = selected + self.flight_member.loadout.pylons[self.pylon.number] = selected if selected is None: logging.debug(f"Pylon {self.pylon.number} emptied") @@ -70,5 +74,9 @@ class QPylonEditor(QComboBox): return "None" return weapon.name + def set_flight_member(self, flight_member: FlightMember) -> None: + self.flight_member = flight_member + self.set_from(self.flight_member.loadout) + def set_from(self, loadout: Loadout) -> None: self.setCurrentText(self.matching_weapon_name(loadout)) diff --git a/qt_ui/windows/mission/flight/payload/propertycheckbox.py b/qt_ui/windows/mission/flight/payload/propertycheckbox.py index 5f2623e3..f5d1caee 100644 --- a/qt_ui/windows/mission/flight/payload/propertycheckbox.py +++ b/qt_ui/windows/mission/flight/payload/propertycheckbox.py @@ -1,21 +1,31 @@ from PySide6.QtWidgets import QCheckBox from dcs.unitpropertydescription import UnitPropertyDescription -from game.ato import Flight +from game.ato.flightmember import FlightMember from .missingpropertydataerror import MissingPropertyDataError class PropertyCheckBox(QCheckBox): - def __init__(self, flight: Flight, prop: UnitPropertyDescription) -> None: + def __init__( + self, flight_member: FlightMember, prop: UnitPropertyDescription + ) -> None: super().__init__() - self.flight = flight + self.flight_member = flight_member self.prop = prop if prop.default is None: raise MissingPropertyDataError("default cannot be None") - self.setChecked(self.flight.props.get(self.prop.identifier, self.prop.default)) + self.setChecked( + self.flight_member.properties.get(self.prop.identifier, self.prop.default) + ) self.toggled.connect(self.on_toggle) def on_toggle(self, checked: bool) -> None: - self.flight.props[self.prop.identifier] = checked + self.flight_member.properties[self.prop.identifier] = checked + + def set_flight_member(self, flight_member: FlightMember) -> None: + self.flight_member = flight_member + self.setChecked( + self.flight_member.properties.get(self.prop.identifier, self.prop.default) + ) diff --git a/qt_ui/windows/mission/flight/payload/propertycombobox.py b/qt_ui/windows/mission/flight/payload/propertycombobox.py index 4780b881..0d407982 100644 --- a/qt_ui/windows/mission/flight/payload/propertycombobox.py +++ b/qt_ui/windows/mission/flight/payload/propertycombobox.py @@ -1,14 +1,16 @@ from PySide6.QtWidgets import QComboBox from dcs.unitpropertydescription import UnitPropertyDescription -from game.ato import Flight +from game.ato.flightmember import FlightMember from .missingpropertydataerror import MissingPropertyDataError class PropertyComboBox(QComboBox): - def __init__(self, flight: Flight, prop: UnitPropertyDescription) -> None: + def __init__( + self, flight_member: FlightMember, prop: UnitPropertyDescription + ) -> None: super().__init__() - self.flight = flight + self.flight_member = flight_member self.prop = prop if prop.values is None: @@ -16,7 +18,9 @@ class PropertyComboBox(QComboBox): if prop.default is None: raise MissingPropertyDataError("default cannot be None") - current_value = self.flight.props.get(self.prop.identifier, self.prop.default) + current_value = self.flight_member.properties.get( + self.prop.identifier, self.prop.default + ) for ident, text in self.prop.values.items(): self.addItem(text, ident) @@ -26,4 +30,12 @@ class PropertyComboBox(QComboBox): self.currentIndexChanged.connect(self.on_selection_changed) def on_selection_changed(self, _index: int) -> None: - self.flight.props[self.prop.identifier] = self.currentData() + self.flight_member.properties[self.prop.identifier] = self.currentData() + + def set_flight_member(self, flight_member: FlightMember) -> None: + self.flight_member = flight_member + self.setCurrentText( + self.flight_member.properties.get( + self.prop.identifier, self.prop.values[self.prop.default] + ) + ) diff --git a/qt_ui/windows/mission/flight/payload/propertyeditor.py b/qt_ui/windows/mission/flight/payload/propertyeditor.py index d63a21d2..c17f8cea 100644 --- a/qt_ui/windows/mission/flight/payload/propertyeditor.py +++ b/qt_ui/windows/mission/flight/payload/propertyeditor.py @@ -1,9 +1,11 @@ import logging +from typing import Callable from PySide2.QtWidgets import QGridLayout, QLabel from dcs.unitpropertydescription import UnitPropertyDescription from game.ato import Flight +from game.ato.flightmember import FlightMember from .missingpropertydataerror import MissingPropertyDataError from .propertycheckbox import PropertyCheckBox from .propertycombobox import PropertyComboBox @@ -16,9 +18,10 @@ class UnhandledControlTypeError(RuntimeError): class PropertyEditor(QGridLayout): - def __init__(self, flight: Flight) -> None: + def __init__(self, flight: Flight, flight_member: FlightMember) -> None: super().__init__() - self.flight = flight + self.flight_member = flight_member + self.flight_member_update_listeners: list[Callable[[FlightMember], None]] = [] for row, prop in enumerate(flight.unit_type.iter_props()): if prop.label is None: @@ -56,12 +59,23 @@ class PropertyEditor(QGridLayout): # "checkbox", "comboList", "groupbox", "label", "slider", "spinbox" match prop.control: case "checkbox": - return PropertyCheckBox(self.flight, prop) + widget = PropertyCheckBox(self.flight_member, prop) + self.flight_member_update_listeners.append(widget.set_flight_member) + return widget case "comboList": - return PropertyComboBox(self.flight, prop) + widget = PropertyComboBox(self.flight_member, prop) + self.flight_member_update_listeners.append(widget.set_flight_member) + return widget case "groupbox" | "label": return None case "slider" | "spinbox": - return PropertySpinBox(self.flight, prop) + widget = PropertySpinBox(self.flight_member, prop) + self.flight_member_update_listeners.append(widget.set_flight_member) + return widget case _: raise UnhandledControlTypeError(prop.control) + + def set_flight_member(self, flight_member: FlightMember) -> None: + self.flight_member = flight_member + for listener in self.flight_member_update_listeners: + listener(self.flight_member) diff --git a/qt_ui/windows/mission/flight/payload/propertyspinbox.py b/qt_ui/windows/mission/flight/payload/propertyspinbox.py index 71a0e41f..f9a52913 100644 --- a/qt_ui/windows/mission/flight/payload/propertyspinbox.py +++ b/qt_ui/windows/mission/flight/payload/propertyspinbox.py @@ -1,14 +1,16 @@ from PySide6.QtWidgets import QSpinBox from dcs.unitpropertydescription import UnitPropertyDescription -from game.ato import Flight +from game.ato.flightmember import FlightMember from .missingpropertydataerror import MissingPropertyDataError class PropertySpinBox(QSpinBox): - def __init__(self, flight: Flight, prop: UnitPropertyDescription) -> None: + def __init__( + self, flight_member: FlightMember, prop: UnitPropertyDescription + ) -> None: super().__init__() - self.flight = flight + self.flight_member = flight_member self.prop = prop if prop.minimum is None: @@ -20,9 +22,17 @@ class PropertySpinBox(QSpinBox): self.setMinimum(prop.minimum) self.setMaximum(prop.maximum) - self.setValue(self.flight.props.get(self.prop.identifier, self.prop.default)) + self.setValue( + self.flight_member.properties.get(self.prop.identifier, self.prop.default) + ) self.valueChanged.connect(self.on_value_changed) def on_value_changed(self, value: int) -> None: - self.flight.props[self.prop.identifier] = value + self.flight_member.properties[self.prop.identifier] = value + + def set_flight_member(self, flight_member: FlightMember) -> None: + self.flight_member = flight_member + self.setValue( + self.flight_member.properties.get(self.prop.identifier, self.prop.default) + ) diff --git a/qt_ui/windows/mission/flight/settings/QFlightSlotEditor.py b/qt_ui/windows/mission/flight/settings/QFlightSlotEditor.py index dc421180..3c9bd96d 100644 --- a/qt_ui/windows/mission/flight/settings/QFlightSlotEditor.py +++ b/qt_ui/windows/mission/flight/settings/QFlightSlotEditor.py @@ -1,5 +1,5 @@ import logging -from typing import Optional, Callable +from typing import Optional, Callable, Union from PySide2.QtCore import Signal, QModelIndex from PySide2.QtWidgets import ( @@ -16,6 +16,8 @@ from PySide2.QtWidgets import ( from game import Game from game.ato.flight import Flight from game.ato.flightroster import FlightRoster +from game.ato.iflightroster import IFlightRoster +from game.squadrons import Squadron from game.squadrons.pilot import Pilot from qt_ui.models import PackageModel @@ -23,8 +25,11 @@ from qt_ui.models import PackageModel class PilotSelector(QComboBox): available_pilots_changed = Signal() - def __init__(self, roster: Optional[FlightRoster], idx: int) -> None: + def __init__( + self, squadron: Union[Squadron, None], roster: Optional[IFlightRoster], idx: int + ) -> None: super().__init__() + self.squadron = squadron self.roster = roster self.pilot_index = idx self.rebuild() @@ -40,10 +45,13 @@ class PilotSelector(QComboBox): self.setDisabled(True) return + if self.squadron is None: + raise RuntimeError("squadron cannot be None if roster is set") + self.setEnabled(True) self.addItem("Unassigned", None) - choices = list(self.roster.squadron.available_pilots) - current_pilot = self.roster.pilots[self.pilot_index] + choices = list(self.squadron.available_pilots) + current_pilot = self.roster.pilot_at(self.pilot_index) if current_pilot is not None: choices.append(current_pilot) # Put players first, otherwise alphabetically. @@ -71,23 +79,26 @@ class PilotSelector(QComboBox): # The roster resize is handled separately, so we have no pilots to remove. return pilot = self.itemData(index) - if pilot == self.roster.pilots[self.pilot_index]: + if pilot == self.roster.pilot_at(self.pilot_index): return self.roster.set_pilot(self.pilot_index, pilot) self.available_pilots_changed.emit() - def replace(self, new_roster: Optional[FlightRoster]) -> None: + def replace(self, squadron: Squadron, new_roster: Optional[FlightRoster]) -> None: + self.squadron = squadron self.roster = new_roster self.rebuild() class PilotControls(QHBoxLayout): - def __init__(self, roster: Optional[FlightRoster], idx: int) -> None: + def __init__( + self, squadron: Union[Squadron, None], roster: Optional[FlightRoster], idx: int + ) -> None: super().__init__() self.roster = roster self.pilot_index = idx - self.selector = PilotSelector(roster, idx) + self.selector = PilotSelector(squadron, roster, idx) self.selector.currentIndexChanged.connect(self.on_pilot_changed) self.addWidget(self.selector) @@ -95,8 +106,8 @@ class PilotControls(QHBoxLayout): self.player_checkbox.setToolTip("Checked if this pilot is a player.") self.on_pilot_changed(self.selector.currentIndex()) enabled = False - if self.roster is not None and self.roster.squadron is not None: - enabled = self.roster.squadron.aircraft.flyable + if self.roster is not None and squadron is not None: + enabled = squadron.aircraft.flyable self.player_checkbox.setEnabled(enabled) self.addWidget(self.player_checkbox) @@ -106,7 +117,7 @@ class PilotControls(QHBoxLayout): def pilot(self) -> Optional[Pilot]: if self.roster is None or self.pilot_index >= self.roster.max_size: return None - return self.roster.pilots[self.pilot_index] + return self.roster.pilot_at(self.pilot_index) def on_player_toggled(self, checked: bool) -> None: pilot = self.pilot @@ -145,19 +156,19 @@ class PilotControls(QHBoxLayout): finally: self.player_checkbox.blockSignals(False) - def replace(self, new_roster: Optional[FlightRoster]) -> None: + def replace(self, squadron: Squadron, new_roster: Optional[FlightRoster]) -> None: self.roster = new_roster if self.roster is None or self.pilot_index >= self.roster.max_size: self.disable_and_clear() else: self.enable_and_reset() - self.selector.replace(new_roster) + self.selector.replace(squadron, new_roster) class FlightRosterEditor(QVBoxLayout): MAX_PILOTS = 4 - def __init__(self, roster: Optional[FlightRoster]) -> None: + def __init__(self, squadron: Union[Squadron, None], roster: Union[IFlightRoster, None]) -> None: super().__init__() self.roster = roster @@ -170,7 +181,7 @@ class FlightRosterEditor(QVBoxLayout): return callback - controls = PilotControls(roster, pilot_idx) + controls = PilotControls(squadron, roster, pilot_idx) controls.selector.available_pilots_changed.connect( make_reset_callback(pilot_idx) ) @@ -193,15 +204,17 @@ class FlightRosterEditor(QVBoxLayout): for controls in self.pilot_controls[new_size:]: controls.disable_and_clear() - def replace(self, new_roster: Optional[FlightRoster]) -> None: + def replace(self, squadron: Squadron, new_roster: Optional[FlightRoster]) -> None: if self.roster is not None: self.roster.clear() self.roster = new_roster for controls in self.pilot_controls: - controls.replace(new_roster) + controls.replace(squadron, new_roster) class QFlightSlotEditor(QGroupBox): + flight_resized = Signal(int) + def __init__(self, package_model: PackageModel, flight: Flight, game: Game): super().__init__("Slots") self.package_model = package_model @@ -228,7 +241,7 @@ class QFlightSlotEditor(QGroupBox): layout.addWidget(QLabel(str(self.flight.squadron)), 1, 1) layout.addWidget(QLabel("Assigned pilots:"), 2, 0) - self.roster_editor = FlightRosterEditor(flight.roster) + self.roster_editor = FlightRosterEditor(flight.squadron, flight.roster) layout.addLayout(self.roster_editor, 2, 1) self.setLayout(layout) @@ -251,3 +264,4 @@ class QFlightSlotEditor(QGroupBox): self.flight.resize(old_count) return self.roster_editor.resize(new_count) + self.flight_resized.emit(new_count) diff --git a/qt_ui/windows/mission/flight/settings/QGeneralFlightSettingsTab.py b/qt_ui/windows/mission/flight/settings/QGeneralFlightSettingsTab.py index 1fa19b09..ca4e86d8 100644 --- a/qt_ui/windows/mission/flight/settings/QGeneralFlightSettingsTab.py +++ b/qt_ui/windows/mission/flight/settings/QGeneralFlightSettingsTab.py @@ -19,7 +19,7 @@ from qt_ui.windows.mission.flight.waypoints.QFlightWaypointList import ( class QGeneralFlightSettingsTab(QFrame): - on_flight_settings_changed = Signal() + flight_size_changed = Signal() def __init__( self, @@ -30,13 +30,16 @@ class QGeneralFlightSettingsTab(QFrame): ): super().__init__() + self.flight_slot_editor = QFlightSlotEditor(package_model, flight, game.game) + self.flight_slot_editor.flight_resized.connect(self.flight_size_changed) + widgets = [ QFlightTypeTaskInfo(flight), QCommsEditor(flight, game), FlightPlanPropertiesGroup( game.game, package_model, flight, flight_wpt_list ), - QFlightSlotEditor(package_model, flight, game.game), + self.flight_slot_editor, QFlightStartType(package_model, flight), QFlightCustomName(flight), ] @@ -45,6 +48,7 @@ class QGeneralFlightSettingsTab(QFrame): for w in widgets: layout.addWidget(w, row, 0) row += 1 + vstretch = QVBoxLayout() vstretch.addStretch() layout.addLayout(vstretch, row, 0) diff --git a/qt_ui/windows/mission/flight/waypoints/QFlightWaypointTab.py b/qt_ui/windows/mission/flight/waypoints/QFlightWaypointTab.py index ebda1ebc..a288a457 100644 --- a/qt_ui/windows/mission/flight/waypoints/QFlightWaypointTab.py +++ b/qt_ui/windows/mission/flight/waypoints/QFlightWaypointTab.py @@ -226,9 +226,11 @@ class QFlightWaypointTab(QFrame): QMessageBox.critical( self, "Could not recreate flight", str(ex), QMessageBox.Ok ) - if not self.flight.loadout.is_custom: - self.flight.loadout = Loadout.default_for(self.flight) - self.loadout_changed.emit() + for member in self.flight.iter_members(): + if not member.loadout.is_custom: + member.loadout = Loadout.default_for(self.flight) + self.loadout_changed.emit() + self.flight_waypoint_list.update_list() self.on_change() def on_change(self):