diff --git a/game/data/weapons.py b/game/data/weapons.py
new file mode 100644
index 00000000..27dd3a8d
--- /dev/null
+++ b/game/data/weapons.py
@@ -0,0 +1,80 @@
+from __future__ import annotations
+
+import inspect
+from dataclasses import dataclass
+from typing import Dict, Iterator, Set, Tuple, Type, Union, cast
+
+from dcs.unitgroup import FlyingGroup
+from dcs.unittype import FlyingType
+
+
+PydcsWeapon = Dict[str, Union[int, str]]
+PydcsWeaponAssignment = Tuple[int, PydcsWeapon]
+
+
+@dataclass(frozen=True)
+class Weapon:
+ """Wraps a pydcs weapon dict in a hashable type."""
+
+ cls_id: str
+ name: str
+ weight: int
+
+ @property
+ def as_pydcs(self) -> PydcsWeapon:
+ return {
+ "clsid": self.cls_id,
+ "name": self.name,
+ "weight": self.weight,
+ }
+
+ @classmethod
+ def from_pydcs(cls, weapon_data: PydcsWeapon) -> Weapon:
+ return cls(
+ cast(str, weapon_data["clsid"]),
+ cast(str, weapon_data["name"]),
+ cast(int, weapon_data["weight"])
+ )
+
+
+@dataclass(frozen=True)
+class Pylon:
+ number: int
+ allowed: Set[Weapon]
+
+ def can_equip(self, weapon: Weapon) -> bool:
+ return weapon in self.allowed
+
+ def equip(self, group: FlyingGroup, weapon: Weapon) -> None:
+ if not self.can_equip(weapon):
+ raise ValueError(f"Pylon {self.number} cannot equip {weapon.name}")
+ group.load_pylon(self.make_pydcs_assignment(weapon), self.number)
+
+ def make_pydcs_assignment(self, weapon: Weapon) -> PydcsWeaponAssignment:
+ return self.number, weapon.as_pydcs
+
+ @classmethod
+ def for_aircraft(cls, aircraft: Type[FlyingType], number: int) -> Pylon:
+ # In pydcs these are all arbitrary inner classes of the aircraft type.
+ # The only way to identify them is by their name.
+ pylons = [v for v in aircraft.__dict__.values() if
+ inspect.isclass(v) and v.__name__.startswith("Pylon")]
+
+ # And that Pylon class has members with irrelevant names that have
+ # values of (pylon number, allowed weapon).
+ allowed = set()
+ for pylon in pylons:
+ for key, value in pylon.__dict__.items():
+ if key.startswith("__"):
+ continue
+ pylon_number, weapon = value
+ if pylon_number != number:
+ continue
+ allowed.add(Weapon.from_pydcs(weapon))
+
+ return cls(number, allowed)
+
+ @classmethod
+ def iter_pylons(cls, aircraft: Type[FlyingType]) -> Iterator[Pylon]:
+ for pylon in sorted(list(aircraft.pylons)):
+ yield cls.for_aircraft(aircraft, pylon)
diff --git a/gen/aircraft.py b/gen/aircraft.py
index 3c8065bf..78fba19e 100644
--- a/gen/aircraft.py
+++ b/gen/aircraft.py
@@ -74,6 +74,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
from game.factions.faction import Faction
from game.settings import Settings
from game.theater.controlpoint import (
@@ -902,22 +903,20 @@ class AircraftConflictGenerator:
else:
assert False
- def _setup_custom_payload(self, flight, group:FlyingGroup):
- if flight.use_custom_loadout:
+ @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__())
- for p in group.units:
- p.pylons.clear()
+ logging.info("Custom loadout for flight : " + flight.__repr__())
+ for p in group.units:
+ p.pylons.clear()
- for key in flight.loadout.keys():
- if "Pylon" + key in flight.unit_type.__dict__.keys():
- print(flight.loadout)
- weapon_dict = flight.unit_type.__dict__["Pylon" + key].__dict__
- if flight.loadout[key] in weapon_dict.keys():
- weapon = weapon_dict[flight.loadout[key]]
- group.load_pylon(weapon, int(key))
- else:
- logging.warning("Pylon not found ! => Pylon" + key + " on " + str(flight.unit_type))
+ for pylon_number, weapon in flight.loadout.items():
+ if weapon is None:
+ continue
+ pylon = Pylon.for_aircraft(flight.unit_type, pylon_number)
+ pylon.equip(group, weapon)
def clear_parking_slots(self) -> None:
for cp in self.game.theater.controlpoints:
diff --git a/gen/flights/flight.py b/gen/flights/flight.py
index 0b784025..3125d6e1 100644
--- a/gen/flights/flight.py
+++ b/gen/flights/flight.py
@@ -1,5 +1,6 @@
from __future__ import annotations
+from collections import defaultdict
from datetime import timedelta
from enum import Enum
from typing import Dict, List, Optional, TYPE_CHECKING, Type
@@ -9,6 +10,7 @@ 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
@@ -148,7 +150,7 @@ class Flight:
self.flight_type = flight_type
# TODO: Replace with FlightPlan.
self.targets: List[MissionTarget] = []
- self.loadout: Dict[str, str] = {}
+ self.loadout: Dict[int, Optional[Weapon]] = {}
self.start_type = start_type
self.use_custom_loadout = False
self.client_count = 0
diff --git a/qt_ui/windows/mission/flight/payload/QLoadoutEditor.py b/qt_ui/windows/mission/flight/payload/QLoadoutEditor.py
index 1f18c293..35617927 100644
--- a/qt_ui/windows/mission/flight/payload/QLoadoutEditor.py
+++ b/qt_ui/windows/mission/flight/payload/QLoadoutEditor.py
@@ -2,6 +2,7 @@ import inspect
from PySide2.QtWidgets import QLabel, QHBoxLayout, QGroupBox, QSpinBox, QGridLayout, QVBoxLayout, QSizePolicy
+from game.data.weapons import Pylon
from qt_ui.windows.mission.flight.payload.QPylonEditor import QPylonEditor
@@ -19,16 +20,12 @@ class QLoadoutEditor(QGroupBox):
hboxLayout = QVBoxLayout(self)
layout = QGridLayout(self)
- pylons = [v for v in self.flight.unit_type.__dict__.values() if inspect.isclass(v) and v.__name__.startswith("Pylon")]
- for i, pylon in enumerate(pylons):
- label = QLabel("{}".format(pylon.__name__[len("Pylon"):]))
- label.setSizePolicy(QSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed))
+ for i, pylon in enumerate(Pylon.iter_pylons(self.flight.unit_type)):
+ label = QLabel(f"{pylon.number}")
+ label.setSizePolicy(
+ QSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed))
layout.addWidget(label, i, 0)
- try:
- pylon_number = int(pylon.__name__.split("Pylon")[1])
- except:
- pylon_number = i+1
- layout.addWidget(QPylonEditor(flight, pylon, pylon_number), i, 1)
+ layout.addWidget(QPylonEditor(flight, pylon), i, 1)
hboxLayout.addLayout(layout)
hboxLayout.addStretch()
diff --git a/qt_ui/windows/mission/flight/payload/QPylonEditor.py b/qt_ui/windows/mission/flight/payload/QPylonEditor.py
index 87a7c49a..449229cb 100644
--- a/qt_ui/windows/mission/flight/payload/QPylonEditor.py
+++ b/qt_ui/windows/mission/flight/payload/QPylonEditor.py
@@ -1,38 +1,37 @@
import logging
+import operator
+from typing import Optional
-from PySide2.QtWidgets import QWidget, QSpinBox, QComboBox
+from PySide2.QtWidgets import QComboBox
+
+from game.data.weapons import Pylon, Weapon
+from gen.flights.flight import Flight
class QPylonEditor(QComboBox):
- def __init__(self, flight, pylon, pylon_number):
- super(QPylonEditor, self).__init__()
- self.pylon = pylon
- self.pylon_number = pylon_number
+ def __init__(self, flight: Flight, pylon: Pylon) -> None:
+ super().__init__()
self.flight = flight
+ self.pylon = pylon
- self.possible_loadout = [i for i in self.pylon.__dict__.keys() if i[:2] != '__']
+ current = self.flight.loadout.get(self.pylon.number)
- if not str(self.pylon_number) in self.flight.loadout.keys():
- self.flight.loadout[str(self.pylon_number)] = ""
-
- self.addItem("None")
- for i,k in enumerate(self.possible_loadout):
- self.addItem(str(self.pylon.__dict__[k][1]["name"]))
- if self.flight.loadout[str(self.pylon_number)] == str(k):
+ self.addItem("None", None)
+ allowed = sorted(pylon.allowed, key=operator.attrgetter("name"))
+ for i, weapon in enumerate(allowed):
+ self.addItem(weapon.name, weapon)
+ if current == weapon:
self.setCurrentIndex(i + 1)
- self.currentTextChanged.connect(self.on_pylon_change)
+ self.currentIndexChanged.connect(self.on_pylon_change)
def on_pylon_change(self):
- selected = self.currentText()
- if selected == "None":
- logging.info("Pylon " + str(self.pylon_number) + " emptied")
- self.flight.loadout[str(self.pylon_number)] = ""
- else:
- logging.info("Pylon " + str(self.pylon_number) + " changed to " + selected)
- for i, k in enumerate(self.possible_loadout):
- if selected == str(self.pylon.__dict__[k][1]["name"]):
- self.flight.loadout[str(self.pylon_number)] = str(k)
- break
+ selected: Optional[Weapon] = self.currentData()
+ self.flight.loadout[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}")