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.
This commit is contained in:
Dan Albert 2021-05-17 01:01:11 -07:00
parent 6ca175345f
commit 0e3bc1ce43
7 changed files with 190 additions and 286 deletions

View File

@ -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:

View File

@ -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:

View File

@ -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

121
gen/flights/loadouts.py Normal file
View File

@ -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)

View File

@ -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()

View File

@ -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)

View File

@ -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 == "<CLEAN>":
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 == "<CLEAN>":
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))