From 0e3bc1ce43fbfda1ed153169bfc2cc1b82f57894 Mon Sep 17 00:00:00 2001 From: Dan Albert Date: Mon, 17 May 2021 01:01:11 -0700 Subject: [PATCH] Loadout implementation cleanup. Loadout selection no longer has two (disagreeing) implementations. What the UI shows is now what the miz will have. We now store the chosen layout in the Flight *always*, not just for custom loadouts. This means that we do loadout lookups at the start of each turn, but the data is cached in pydcs. Era-specific loadout degradation is still done at generation (and presentation) time. This is so that players can toggle that option and have it affect the *current* turn, rather than the next one. --- game/db.py | 143 +----------------- gen/aircraft.py | 94 +++--------- gen/flights/flight.py | 8 +- gen/flights/loadouts.py | 121 +++++++++++++++ .../flight/payload/QFlightPayloadTab.py | 15 +- .../mission/flight/payload/QLoadoutEditor.py | 21 ++- .../mission/flight/payload/QPylonEditor.py | 74 +++------ 7 files changed, 190 insertions(+), 286 deletions(-) create mode 100644 gen/flights/loadouts.py diff --git a/game/db.py b/game/db.py index 91b1c138..980f1461 100644 --- a/game/db.py +++ b/game/db.py @@ -1,8 +1,8 @@ +import json from datetime import datetime from enum import Enum -from typing import Dict, List, Optional, Tuple, Type, Union -import json from pathlib import Path +from typing import Dict, List, Optional, Tuple, Type, Union from dcs.countries import country_dict from dcs.helicopters import ( @@ -62,7 +62,6 @@ from dcs.planes import ( F_4E, F_5E_3, F_86F_Sabre, - F_A_18C, IL_76MD, IL_78M, JF_17, @@ -72,7 +71,6 @@ from dcs.planes import ( KC_135, KC135MPRS, KJ_2000, - L_39C, L_39ZA, MQ_9_Reaper, M_2000C, @@ -92,7 +90,6 @@ from dcs.planes import ( P_47D_40, P_51D, P_51D_30_NA, - PlaneType, RQ_1A_Predator, S_3B, S_3B_Tanker, @@ -103,7 +100,6 @@ from dcs.planes import ( Su_24MR, Su_25, Su_25T, - Su_25TM, Su_27, Su_30, Su_33, @@ -1178,141 +1174,6 @@ COMMON_OVERRIDE = { AWACS: "AEW&C", } -""" -This is a list of mappings from the FlightType of a Flight to the type of payload defined in the -resources/payloads/UNIT_TYPE.lua file. A Flight has no concept of a PyDCS task, so COMMON_OVERRIDE cannot be -used here. This is used in the payload editor, for setting the default loadout of an object. -The left element is the FlightType name, and the right element is a tuple containing what is used in the lua file. -Some aircraft differ from the standard loadout names, so those have been included here too. -The priority goes from first to last - the first element in the tuple will be tried first, then the second, etc. -""" - -EXPANDED_TASK_PAYLOAD_OVERRIDE = { - "TARCAP": ("CAP HEAVY", "CAP"), - "BARCAP": ("CAP HEAVY", "CAP"), - "CAS": ("CAS MAVERICK F", "CAS"), - "INTERCEPTION": ("CAP HEAVY", "CAP"), - "STRIKE": ("STRIKE",), - "ANTISHIP": ("ANTISHIP",), - "SEAD": ("SEAD",), - "DEAD": ("SEAD",), - "ESCORT": ("CAP HEAVY", "CAP"), - "BAI": ("BAI", "CAS MAVERICK F", "CAS"), - "SWEEP": ("CAP HEAVY", "CAP"), - "OCA_RUNWAY": ("RUNWAY_ATTACK", "RUNWAY_STRIKE", "STRIKE"), - "OCA_AIRCRAFT": ("OCA", "CAS MAVERICK F", "CAS"), - "TRANSPORT": (), -} - -PLANE_PAYLOAD_OVERRIDES: Dict[Type[PlaneType], Dict[Type[Task], str]] = { - B_1B: COMMON_OVERRIDE, - B_52H: COMMON_OVERRIDE, - F_117A: COMMON_OVERRIDE, - F_15E: COMMON_OVERRIDE, - FA_18C_hornet: { - CAP: "CAP HEAVY", - Intercept: "CAP HEAVY", - CAS: "CAS MAVERICK F", - PinpointStrike: "STRIKE", - SEAD: "SEAD", - AntishipStrike: "ANTISHIP", - GroundAttack: "STRIKE", - Escort: "CAP HEAVY", - FighterSweep: "CAP HEAVY", - }, - F_A_18C: { - CAP: "CAP HEAVY", - Intercept: "CAP HEAVY", - CAS: "CAS MAVERICK F", - PinpointStrike: "STRIKE", - SEAD: "SEAD", - AntishipStrike: "ANTISHIP", - GroundAttack: "STRIKE", - Escort: "CAP HEAVY", - FighterSweep: "CAP HEAVY", - }, - Tu_160: { - PinpointStrike: "Kh-65*12", - }, - Tu_22M3: COMMON_OVERRIDE, - Tu_95MS: COMMON_OVERRIDE, - A_10A: COMMON_OVERRIDE, - A_10C: COMMON_OVERRIDE, - A_10C_2: COMMON_OVERRIDE, - AV8BNA: COMMON_OVERRIDE, - C_101CC: COMMON_OVERRIDE, - F_5E_3: COMMON_OVERRIDE, - F_14A_135_GR: COMMON_OVERRIDE, - F_14B: COMMON_OVERRIDE, - F_15C: COMMON_OVERRIDE, - F_22A: COMMON_OVERRIDE, - F_16C_50: COMMON_OVERRIDE, - JF_17: COMMON_OVERRIDE, - M_2000C: COMMON_OVERRIDE, - MiG_15bis: COMMON_OVERRIDE, - MiG_19P: COMMON_OVERRIDE, - MiG_21Bis: COMMON_OVERRIDE, - AJS37: COMMON_OVERRIDE, - Su_25T: COMMON_OVERRIDE, - Su_25: COMMON_OVERRIDE, - Su_27: COMMON_OVERRIDE, - Su_33: COMMON_OVERRIDE, - MiG_29A: COMMON_OVERRIDE, - MiG_29G: COMMON_OVERRIDE, - MiG_29S: COMMON_OVERRIDE, - Su_24M: COMMON_OVERRIDE, - Su_30: COMMON_OVERRIDE, - Su_34: COMMON_OVERRIDE, - Su_57: COMMON_OVERRIDE, - MiG_23MLD: COMMON_OVERRIDE, - MiG_27K: COMMON_OVERRIDE, - Tornado_GR4: COMMON_OVERRIDE, - Tornado_IDS: COMMON_OVERRIDE, - Mirage_2000_5: COMMON_OVERRIDE, - MiG_31: COMMON_OVERRIDE, - S_3B: COMMON_OVERRIDE, - SA342M: COMMON_OVERRIDE, - SA342L: COMMON_OVERRIDE, - SA342Mistral: COMMON_OVERRIDE, - Mi_8MT: COMMON_OVERRIDE, - Mi_24V: COMMON_OVERRIDE, - Mi_28N: COMMON_OVERRIDE, - Ka_50: COMMON_OVERRIDE, - L_39ZA: COMMON_OVERRIDE, - L_39C: COMMON_OVERRIDE, - Su_17M4: COMMON_OVERRIDE, - F_4E: COMMON_OVERRIDE, - P_47D_30: COMMON_OVERRIDE, - P_47D_30bl1: COMMON_OVERRIDE, - P_47D_40: COMMON_OVERRIDE, - B_17G: COMMON_OVERRIDE, - P_51D: COMMON_OVERRIDE, - P_51D_30_NA: COMMON_OVERRIDE, - FW_190D9: COMMON_OVERRIDE, - FW_190A8: COMMON_OVERRIDE, - Bf_109K_4: COMMON_OVERRIDE, - I_16: COMMON_OVERRIDE, - SpitfireLFMkIXCW: COMMON_OVERRIDE, - SpitfireLFMkIX: COMMON_OVERRIDE, - A_20G: COMMON_OVERRIDE, - A_4E_C: COMMON_OVERRIDE, - MB_339PAN: COMMON_OVERRIDE, - OH_58D: COMMON_OVERRIDE, - F_16A: COMMON_OVERRIDE, - MQ_9_Reaper: COMMON_OVERRIDE, - RQ_1A_Predator: COMMON_OVERRIDE, - WingLoong_I: COMMON_OVERRIDE, - AH_1W: COMMON_OVERRIDE, - AH_64D: COMMON_OVERRIDE, - AH_64A: COMMON_OVERRIDE, - SH_60B: COMMON_OVERRIDE, - Hercules: COMMON_OVERRIDE, - F_86F_Sabre: COMMON_OVERRIDE, - Su_25TM: { - SEAD: "Kh-31P*2_Kh-25ML*4_R-73*2_L-081_MPS410", - }, -} - """ Aircraft livery overrides. Syntax as follows: diff --git a/gen/aircraft.py b/gen/aircraft.py index cce4cdb2..b1d35486 100644 --- a/gen/aircraft.py +++ b/gen/aircraft.py @@ -61,12 +61,10 @@ from dcs.task import ( OptReactOnThreat, OptRestrictJettison, OrbitAction, - PinpointStrike, RunwayAttack, SEAD, StartCommand, Targets, - Task, Transport, WeaponType, ) @@ -77,7 +75,7 @@ from dcs.unittype import FlyingType, UnitType from game import db from game.data.cap_capabilities_db import GUNFIGHTERS -from game.data.weapons import Pylon, Weapon +from game.data.weapons import Pylon from game.factions.faction import Faction from game.settings import Settings from game.theater.controlpoint import ( @@ -728,43 +726,13 @@ class AircraftConflictGenerator: def _setup_group( self, group: FlyingGroup, - loadout_for_task: Type[Task], package: Package, flight: Flight, dynamic_runways: Dict[str, RunwayData], ) -> None: - did_load_loadout = False unit_type = group.units[0].unit_type - if unit_type in db.PLANE_PAYLOAD_OVERRIDES: - # Clear pylons - for p in group.units: - p.pylons.clear() - - # Now load loadout - if loadout_for_task in db.PLANE_PAYLOAD_OVERRIDES[unit_type]: - payload_name = db.PLANE_PAYLOAD_OVERRIDES[unit_type][loadout_for_task] - group.load_loadout(payload_name) - if not group.units[0].pylons and loadout_for_task == RunwayAttack: - if PinpointStrike in db.PLANE_PAYLOAD_OVERRIDES[unit_type]: - logging.warning( - 'No loadout for "Runway Attack" for the {}, defaulting to Strike loadout'.format( - str(unit_type) - ) - ) - payload_name = db.PLANE_PAYLOAD_OVERRIDES[unit_type][ - PinpointStrike - ] - group.load_loadout(payload_name) - did_load_loadout = True - logging.info( - "Loaded overridden payload for {} - {} for task {}".format( - unit_type, payload_name, loadout_for_task - ) - ) - - if not did_load_loadout: - group.load_task_default_loadout(loadout_for_task) + self._setup_payload(flight, group) if unit_type in db.PLANE_LIVERY_OVERRIDES: for unit_instance in group.units: @@ -976,39 +944,20 @@ class AircraftConflictGenerator: else: assert False - @staticmethod - def _setup_custom_payload(flight: Flight, group: FlyingGroup) -> None: - if not flight.use_custom_loadout: - return - - logging.info("Custom loadout for flight : " + flight.__repr__()) + def _setup_payload(self, flight: Flight, group: FlyingGroup) -> None: for p in group.units: p.pylons.clear() - for pylon_number, weapon in flight.loadout.items(): + loadout = flight.loadout + if self.game.settings.restrict_weapons_by_date: + loadout = loadout.degrade_for_date(flight.unit_type, self.game.date) + + for pylon_number, weapon in loadout.pylons.items(): if weapon is None: continue pylon = Pylon.for_aircraft(flight.unit_type, pylon_number) pylon.equip(group, weapon) - def _degrade_payload_to_era(self, flight: Flight, group: FlyingGroup) -> None: - loadout = dict(group.units[0].pylons) - for pylon_number, clsid in loadout.items(): - weapon = Weapon.from_clsid(clsid["CLSID"]) - if weapon is None: - logging.error(f"Could not find weapon for clsid {clsid}") - continue - - if not weapon.available_on(self.game.date): - pylon = Pylon.for_aircraft(flight.unit_type, pylon_number) - for fallback in weapon.fallbacks: - if not pylon.can_equip(fallback): - continue - if not fallback.available_on(self.game.date): - continue - pylon.equip(group, fallback) - break - def clear_parking_slots(self) -> None: for cp in self.game.theater.controlpoints: for parking_slot in cp.parking_slots: @@ -1237,7 +1186,7 @@ class AircraftConflictGenerator: dynamic_runways: Dict[str, RunwayData], ) -> None: group.task = CAP.name - self._setup_group(group, CAP, package, flight, dynamic_runways) + self._setup_group(group, package, flight, dynamic_runways) if flight.unit_type not in GUNFIGHTERS: ammo_type = OptRTBOnOutOfAmmo.Values.AAM @@ -1254,7 +1203,7 @@ class AircraftConflictGenerator: dynamic_runways: Dict[str, RunwayData], ) -> None: group.task = FighterSweep.name - self._setup_group(group, FighterSweep, package, flight, dynamic_runways) + self._setup_group(group, package, flight, dynamic_runways) if flight.unit_type not in GUNFIGHTERS: ammo_type = OptRTBOnOutOfAmmo.Values.AAM @@ -1271,7 +1220,7 @@ class AircraftConflictGenerator: dynamic_runways: Dict[str, RunwayData], ) -> None: group.task = CAS.name - self._setup_group(group, CAS, package, flight, dynamic_runways) + self._setup_group(group, package, flight, dynamic_runways) self.configure_behavior( group, react_on_threat=OptReactOnThreat.Values.EvadeFire, @@ -1295,7 +1244,7 @@ class AircraftConflictGenerator: # waypoint actions the group may perform. group.task = CAS.name # But we still use the SEAD *loadout*. - self._setup_group(group, SEAD, package, flight, dynamic_runways) + self._setup_group(group, package, flight, dynamic_runways) self.configure_behavior( group, react_on_threat=OptReactOnThreat.Values.EvadeFire, @@ -1312,7 +1261,7 @@ class AircraftConflictGenerator: dynamic_runways: Dict[str, RunwayData], ) -> None: group.task = SEAD.name - self._setup_group(group, SEAD, package, flight, dynamic_runways) + self._setup_group(group, package, flight, dynamic_runways) self.configure_behavior( group, react_on_threat=OptReactOnThreat.Values.EvadeFire, @@ -1329,7 +1278,7 @@ class AircraftConflictGenerator: dynamic_runways: Dict[str, RunwayData], ) -> None: group.task = GroundAttack.name - self._setup_group(group, GroundAttack, package, flight, dynamic_runways) + self._setup_group(group, package, flight, dynamic_runways) self.configure_behavior( group, react_on_threat=OptReactOnThreat.Values.EvadeFire, @@ -1345,7 +1294,7 @@ class AircraftConflictGenerator: dynamic_runways: Dict[str, RunwayData], ) -> None: group.task = AntishipStrike.name - self._setup_group(group, AntishipStrike, package, flight, dynamic_runways) + self._setup_group(group, package, flight, dynamic_runways) self.configure_behavior( group, react_on_threat=OptReactOnThreat.Values.EvadeFire, @@ -1361,7 +1310,7 @@ class AircraftConflictGenerator: dynamic_runways: Dict[str, RunwayData], ) -> None: group.task = RunwayAttack.name - self._setup_group(group, RunwayAttack, package, flight, dynamic_runways) + self._setup_group(group, package, flight, dynamic_runways) self.configure_behavior( group, react_on_threat=OptReactOnThreat.Values.EvadeFire, @@ -1377,7 +1326,7 @@ class AircraftConflictGenerator: dynamic_runways: Dict[str, RunwayData], ) -> None: group.task = CAS.name - self._setup_group(group, CAS, package, flight, dynamic_runways) + self._setup_group(group, package, flight, dynamic_runways) self.configure_behavior( group, react_on_threat=OptReactOnThreat.Values.EvadeFire, @@ -1400,7 +1349,7 @@ class AircraftConflictGenerator: ) return - self._setup_group(group, AWACS, package, flight, dynamic_runways) + self._setup_group(group, package, flight, dynamic_runways) # Awacs task action self.configure_behavior( @@ -1423,7 +1372,7 @@ class AircraftConflictGenerator: # Search Then Engage task, which we have to use instead of the Escort # task for the reasons explained in JoinPointBuilder. group.task = CAP.name - self._setup_group(group, CAP, package, flight, dynamic_runways) + self._setup_group(group, package, flight, dynamic_runways) self.configure_behavior( group, roe=OptROE.Values.OpenFire, restrict_jettison=True ) @@ -1439,7 +1388,7 @@ class AircraftConflictGenerator: # Search Then Engage task, which we have to use instead of the Escort # task for the reasons explained in JoinPointBuilder. group.task = Transport.name - self._setup_group(group, Transport, package, flight, dynamic_runways) + self._setup_group(group, package, flight, dynamic_runways) self.configure_behavior( group, react_on_threat=OptReactOnThreat.Values.EvadeFire, @@ -1540,9 +1489,6 @@ class AircraftConflictGenerator: # Set here rather than when the FlightData is created so they waypoints # have their TOTs set. self.flights[-1].waypoints = [takeoff_point] + flight.points - self._setup_custom_payload(flight, group) - if self.game.settings.restrict_weapons_by_date: - self._degrade_payload_to_era(flight, group) def should_delay_flight(self, flight: Flight, start_time: timedelta) -> bool: if start_time.total_seconds() <= 0: diff --git a/gen/flights/flight.py b/gen/flights/flight.py index bab73cb4..1e3c4d4d 100644 --- a/gen/flights/flight.py +++ b/gen/flights/flight.py @@ -2,19 +2,19 @@ from __future__ import annotations from datetime import timedelta from enum import Enum -from typing import Dict, List, Optional, TYPE_CHECKING, Type +from typing import List, Optional, TYPE_CHECKING, Type from dcs.mapping import Point from dcs.point import MovingPoint, PointAction from dcs.unittype import FlyingType from game import db -from game.data.weapons import Weapon from game.theater.controlpoint import ControlPoint, MissionTarget from game.utils import Distance, meters +from gen.flights.loadouts import Loadout if TYPE_CHECKING: - from game.transfers import Airlift, TransferOrder + from game.transfers import TransferOrder from gen.ato import Package from gen.flights.flightplan import FlightPlan @@ -179,7 +179,7 @@ class Flight: self.flight_type = flight_type # TODO: Replace with FlightPlan. self.targets: List[MissionTarget] = [] - self.loadout: Dict[int, Optional[Weapon]] = {} + self.loadout = Loadout.default_for(self) self.start_type = start_type self.use_custom_loadout = False self.client_count = 0 diff --git a/gen/flights/loadouts.py b/gen/flights/loadouts.py new file mode 100644 index 00000000..0ba756db --- /dev/null +++ b/gen/flights/loadouts.py @@ -0,0 +1,121 @@ +from __future__ import annotations + +import datetime +from typing import Optional, List, Iterator, Type, TYPE_CHECKING, Mapping + +from dcs.unittype import FlyingType + +from game.data.weapons import Weapon, Pylon + +if TYPE_CHECKING: + from gen.flights.flight import Flight + + +class Loadout: + def __init__( + self, + name: str, + pylons: Mapping[int, Optional[Weapon]], + date: Optional[datetime.date], + is_custom: bool = False, + ) -> None: + self.name = name + self.pylons = {k: v for k, v in pylons.items() if v is not None} + self.date = date + self.is_custom = is_custom + + def derive_custom(self, name: str) -> Loadout: + return Loadout(name, self.pylons, self.date, is_custom=True) + + def degrade_for_date( + self, unit_type: Type[FlyingType], date: datetime.date + ) -> Loadout: + if self.date is not None and self.date <= date: + return Loadout(self.name, self.pylons, self.date) + + new_pylons = dict(self.pylons) + for pylon_number, weapon in self.pylons.items(): + if not weapon.available_on(date): + pylon = Pylon.for_aircraft(unit_type, pylon_number) + for fallback in weapon.fallbacks: + if not pylon.can_equip(fallback): + continue + if not fallback.available_on(date): + continue + new_pylons[pylon_number] = fallback + break + return Loadout(f"{self.name} ({date.year})", new_pylons, date) + + @classmethod + def iter_for(cls, flight: Flight) -> Iterator[Loadout]: + # Dict of payload ID (numeric) to: + # + # { + # "name": The name the user set in the ME + # "pylons": List (as a dict) of dicts of: + # {"CLSID": class ID, "num": pylon number} + # "tasks": List (as a dict) of task IDs the payload is used by. + # } + payloads = flight.unit_type.load_payloads() + for payload in payloads["payloads"].values(): + name = payload["name"] + pylons = payload["pylons"] + yield Loadout( + name, + {p["num"]: Weapon.from_clsid(p["CLSID"]) for p in pylons.values()}, + date=None, + ) + + @classmethod + def all_for(cls, flight: Flight) -> List[Loadout]: + return list(cls.iter_for(flight)) + + @classmethod + def default_loadout_names_for(cls, flight: Flight) -> Iterator[str]: + from gen.flights.flight import FlightType + + # This is a list of mappings from the FlightType of a Flight to the type of + # payload defined in the resources/payloads/UNIT_TYPE.lua file. A Flight has no + # concept of a PyDCS task, so COMMON_OVERRIDE cannot be used here. This is used + # in the payload editor, for setting the default loadout of an object. The left + # element is the FlightType name, and the right element is a tuple containing + # what is used in the lua file. Some aircraft differ from the standard loadout + # names, so those have been included here too. The priority goes from first to + # last - the first element in the tuple will be tried first, then the second, + # etc. + yield from { + FlightType.TARCAP: ("CAP HEAVY", "CAP"), + FlightType.BARCAP: ("CAP HEAVY", "CAP"), + FlightType.CAS: ("CAS MAVERICK F", "CAS"), + FlightType.INTERCEPTION: ("CAP HEAVY", "CAP"), + FlightType.STRIKE: ("STRIKE",), + FlightType.ANTISHIP: ("ANTISHIP",), + FlightType.SEAD: ("SEAD",), + FlightType.DEAD: ("SEAD",), + FlightType.ESCORT: ("CAP HEAVY", "CAP"), + FlightType.BAI: ("BAI", "CAS MAVERICK F", "CAS"), + FlightType.SWEEP: ("CAP HEAVY", "CAP"), + FlightType.OCA_RUNWAY: ("RUNWAY_ATTACK", "RUNWAY_STRIKE", "STRIKE"), + FlightType.OCA_AIRCRAFT: ("OCA", "CAS MAVERICK F", "CAS"), + FlightType.TRANSPORT: (), + FlightType.AEWC: (), + }.get(flight.flight_type, []) + + @classmethod + def default_for(cls, flight: Flight) -> Loadout: + # Iterate through each possible payload type for a given aircraft. + # Some aircraft have custom loadouts that in aren't the standard set. + for name in cls.default_loadout_names_for(flight): + # This operation is cached, but must be called before load_by_name will + # work. + flight.unit_type.load_payloads() + payload = flight.unit_type.loadout_by_name(name) + if payload is not None: + return Loadout( + name, + {i: Weapon.from_clsid(d["clsid"]) for i, d in payload}, + date=None, + ) + + # TODO: Try group.load_task_default_loadout(loadout_for_task) + return Loadout("Empty", {}, date=None) diff --git a/qt_ui/windows/mission/flight/payload/QFlightPayloadTab.py b/qt_ui/windows/mission/flight/payload/QFlightPayloadTab.py index 131802dd..2da14527 100644 --- a/qt_ui/windows/mission/flight/payload/QFlightPayloadTab.py +++ b/qt_ui/windows/mission/flight/payload/QFlightPayloadTab.py @@ -1,8 +1,9 @@ -from PySide2.QtWidgets import QFrame, QGridLayout, QLabel from PySide2.QtCore import Qt +from PySide2.QtWidgets import QFrame, QLabel, QVBoxLayout from game import Game from gen.flights.flight import Flight +from gen.flights.loadouts import Loadout from qt_ui.windows.mission.flight.payload.QLoadoutEditor import QLoadoutEditor @@ -11,10 +12,8 @@ class QFlightPayloadTab(QFrame): super(QFlightPayloadTab, self).__init__() self.flight = flight self.payload_editor = QLoadoutEditor(flight, game) - self.init_ui() - def init_ui(self): - layout = QGridLayout() + layout = QVBoxLayout() # Docs Link docsText = QLabel( @@ -23,7 +22,15 @@ class QFlightPayloadTab(QFrame): docsText.setAlignment(Qt.AlignCenter) docsText.setOpenExternalLinks(True) + self.payload_editor.toggled.connect(self.on_custom_toggled) layout.addWidget(self.payload_editor) layout.addWidget(docsText) self.setLayout(layout) + + def on_custom_toggled(self, use_custom: bool) -> None: + if use_custom: + self.flight.loadout = self.flight.loadout.derive_custom("Custom") + else: + self.flight.loadout = Loadout.default_for(self.flight) + self.payload_editor.reset_pylons() diff --git a/qt_ui/windows/mission/flight/payload/QLoadoutEditor.py b/qt_ui/windows/mission/flight/payload/QLoadoutEditor.py index 9d70e82a..ea779c33 100644 --- a/qt_ui/windows/mission/flight/payload/QLoadoutEditor.py +++ b/qt_ui/windows/mission/flight/payload/QLoadoutEditor.py @@ -18,11 +18,9 @@ class QLoadoutEditor(QGroupBox): self.flight = flight self.game = game self.setCheckable(True) - self.setChecked(flight.use_custom_loadout) + self.setChecked(flight.loadout.is_custom) - self.toggled.connect(self.on_toggle) - - hboxLayout = QVBoxLayout(self) + vbox = QVBoxLayout(self) layout = QGridLayout(self) for i, pylon in enumerate(Pylon.iter_pylons(self.flight.unit_type)): @@ -31,16 +29,15 @@ class QLoadoutEditor(QGroupBox): layout.addWidget(label, i, 0) layout.addWidget(QPylonEditor(game, flight, pylon), i, 1) - hboxLayout.addLayout(layout) - hboxLayout.addStretch() - self.setLayout(hboxLayout) + vbox.addLayout(layout) + vbox.addStretch() + self.setLayout(vbox) - if not self.isChecked(): - for i in self.findChildren(QPylonEditor): - i.default_loadout() + for i in self.findChildren(QPylonEditor): + i.set_from(self.flight.loadout) - def on_toggle(self): + def reset_pylons(self) -> None: self.flight.use_custom_loadout = self.isChecked() if not self.isChecked(): for i in self.findChildren(QPylonEditor): - i.default_loadout() + i.set_from(self.flight.loadout) diff --git a/qt_ui/windows/mission/flight/payload/QPylonEditor.py b/qt_ui/windows/mission/flight/payload/QPylonEditor.py index 8591b7d6..9470e2e0 100644 --- a/qt_ui/windows/mission/flight/payload/QPylonEditor.py +++ b/qt_ui/windows/mission/flight/payload/QPylonEditor.py @@ -4,10 +4,10 @@ from typing import Optional from PySide2.QtWidgets import QComboBox -from game import Game, db +from game import Game from game.data.weapons import Pylon, Weapon from gen.flights.flight import Flight -from dcs import weapons_data +from gen.flights.loadouts import Loadout class QPylonEditor(QComboBox): @@ -17,7 +17,7 @@ class QPylonEditor(QComboBox): self.pylon = pylon self.game = game - current = self.flight.loadout.get(self.pylon.number) + current = self.flight.loadout.pylons.get(self.pylon.number) self.addItem("None", None) if self.game.settings.restrict_weapons_by_date: @@ -34,57 +34,29 @@ class QPylonEditor(QComboBox): def on_pylon_change(self): selected: Optional[Weapon] = self.currentData() - self.flight.loadout[self.pylon.number] = selected + self.flight.loadout.pylons[self.pylon.number] = selected if selected is None: logging.debug(f"Pylon {self.pylon.number} emptied") else: logging.debug(f"Pylon {self.pylon.number} changed to {selected.name}") - def default_loadout(self) -> None: - self.flight.unit_type.load_payloads() - self.setCurrentText("None") - pylon_default_weapon = None - historical_weapon = None - loadout = None - # Iterate through each possible payload type for a given aircraft. - # Some aircraft have custom loadouts that in aren't the standard set. - for payload_override in db.EXPANDED_TASK_PAYLOAD_OVERRIDE.get( - self.flight.flight_type.name - ): - if loadout is None: - loadout = self.flight.unit_type.loadout_by_name(payload_override) - if loadout is not None: - for i in loadout: - if i[0] == self.pylon.number: - pylon_default_weapon = i[1]["clsid"] - # TODO: Handle removed pylons better. - if pylon_default_weapon == "": - pylon_default_weapon = None - if pylon_default_weapon is not None: - if self.game.settings.restrict_weapons_by_date: - orig_weapon = Weapon.from_clsid(pylon_default_weapon) - if not orig_weapon.available_on(self.game.date): - for fallback in orig_weapon.fallbacks: - if historical_weapon is None: - if not self.pylon.can_equip(fallback): - continue - if not fallback.available_on(self.game.date): - continue - historical_weapon = fallback - else: - historical_weapon = orig_weapon - if historical_weapon is not None: - self.setCurrentText( - weapons_data.weapon_ids.get(historical_weapon.cls_id).get( - "name" - ) - ) - else: - weapon = weapons_data.weapon_ids.get(pylon_default_weapon) - if weapon is not None: - self.setCurrentText( - weapons_data.weapon_ids.get(pylon_default_weapon).get("name") - ) - else: - self.setCurrentText(pylon_default_weapon) + def weapon_from_loadout(self, loadout: Loadout) -> Optional[Weapon]: + weapon = loadout.pylons.get(self.pylon.number) + if weapon is None: + return None + # TODO: Handle removed pylons better. + if weapon.cls_id == "": + return None + return weapon + + def matching_weapon_name(self, loadout: Loadout) -> str: + if self.game.settings.restrict_weapons_by_date: + loadout = loadout.degrade_for_date(self.flight.unit_type, self.game.date) + weapon = self.weapon_from_loadout(loadout) + if weapon is None: + return "" + return weapon.name + + def set_from(self, loadout: Loadout) -> None: + self.setCurrentText(self.matching_weapon_name(loadout))