From 34945e7ebacf0e7cfa8db04eb4d7403317ac6606 Mon Sep 17 00:00:00 2001 From: Dan Albert Date: Sat, 2 Jan 2021 14:38:07 -0800 Subject: [PATCH] Add date-based loadout restriction. Follow up work: * Data entry. I plan to do the air-to-air missiles in the near term. I covered some variants of the AIM-120, AIM-7, and AIM-9 here, but there are variants of those weapons for each mounting rack that need to be done still, as well as all the non-US weapons. * Arbitrary start dates. https://github.com/Khopa/dcs_liberation/issues/490 --- changelog.md | 1 + game/data/weapons.py | 67 ++++++++++++++++++- game/settings.py | 1 + gen/aircraft.py | 22 +++++- .../mission/flight/payload/QLoadoutEditor.py | 18 +++-- .../mission/flight/payload/QPylonEditor.py | 9 ++- qt_ui/windows/settings/QSettingsWindow.py | 24 +++++++ 7 files changed, 132 insertions(+), 10 deletions(-) diff --git a/changelog.md b/changelog.md index 6b27fd95..49e5d279 100644 --- a/changelog.md +++ b/changelog.md @@ -18,6 +18,7 @@ Saves from 2.3 are not compatible with 2.4. * **[Economy]** FOBs generate only $10M per turn (previously $20M like airbases). * **[Economy]** Carriers and off-map spawns generate no income (previously $20M like airbases). * **[UI]** Multi-SAM objectives now show threat and detection rings per group. +* **[Factions]** Initial implementation of date-based loadout restriction. Only a small number of weapons are currently handled. ## Fixes diff --git a/game/data/weapons.py b/game/data/weapons.py index 27dd3a8d..c76a3078 100644 --- a/game/data/weapons.py +++ b/game/data/weapons.py @@ -1,11 +1,15 @@ from __future__ import annotations +import datetime import inspect +import logging +from collections import defaultdict from dataclasses import dataclass -from typing import Dict, Iterator, Set, Tuple, Type, Union, cast +from typing import Dict, Iterator, Optional, Set, Tuple, Type, Union, cast from dcs.unitgroup import FlyingGroup from dcs.unittype import FlyingType +from dcs.weapons_data import Weapons, weapon_ids PydcsWeapon = Dict[str, Union[int, str]] @@ -20,6 +24,14 @@ class Weapon: name: str weight: int + def available_on(self, date: datetime.date) -> bool: + introduction_year = WEAPON_INTRODUCTION_YEARS.get(self) + if introduction_year is None: + logging.warning( + f"No introduction year for {self}, assuming always available") + return True + return date >= datetime.date(introduction_year, 1, 1) + @property def as_pydcs(self) -> PydcsWeapon: return { @@ -28,6 +40,13 @@ class Weapon: "weight": self.weight, } + @property + def fallbacks(self) -> Iterator[Weapon]: + yield self + fallback = WEAPON_FALLBACK_MAP[self] + if fallback is not None: + yield from fallback.fallbacks + @classmethod def from_pydcs(cls, weapon_data: PydcsWeapon) -> Weapon: return cls( @@ -36,6 +55,13 @@ class Weapon: cast(int, weapon_data["weight"]) ) + @classmethod + def from_clsid(cls, clsid: str) -> Optional[Weapon]: + data = weapon_ids.get(clsid) + if data is None: + return None + return cls.from_pydcs(data) + @dataclass(frozen=True) class Pylon: @@ -53,6 +79,11 @@ class Pylon: def make_pydcs_assignment(self, weapon: Weapon) -> PydcsWeaponAssignment: return self.number, weapon.as_pydcs + def available_on(self, date: datetime.date) -> Iterator[Weapon]: + for weapon in self.allowed: + if weapon.available_on(date): + yield weapon + @classmethod def for_aircraft(cls, aircraft: Type[FlyingType], number: int) -> Pylon: # In pydcs these are all arbitrary inner classes of the aircraft type. @@ -78,3 +109,37 @@ class Pylon: def iter_pylons(cls, aircraft: Type[FlyingType]) -> Iterator[Pylon]: for pylon in sorted(list(aircraft.pylons)): yield cls.for_aircraft(aircraft, pylon) + + +_WEAPON_FALLBACKS = [ + (Weapons.AIM_120C, Weapons.AIM_120B), + (Weapons.AIM_120B, Weapons.AIM_7MH), + (Weapons.AIM_7MH, Weapons.AIM_7M), + (Weapons.AIM_7M, Weapons.AIM_7F), + (Weapons.AIM_7F, Weapons.AIM_7E), + (Weapons.AIM_7M, Weapons.AIM_9X_Sidewinder_IR_AAM), + (Weapons.AIM_9X_Sidewinder_IR_AAM, Weapons.AIM_9P5_Sidewinder_IR_AAM), + (Weapons.AIM_9P5_Sidewinder_IR_AAM, Weapons.AIM_9P_Sidewinder_IR_AAM), + (Weapons.AIM_9P_Sidewinder_IR_AAM, Weapons.AIM_9M_Sidewinder_IR_AAM), + (Weapons.AIM_9M_Sidewinder_IR_AAM, Weapons.AIM_9L_Sidewinder_IR_AAM), +] + +WEAPON_FALLBACK_MAP: Dict[Weapon, Optional[Weapon]] = defaultdict( + lambda: cast(Optional[Weapon], None), + ((Weapon.from_pydcs(a), Weapon.from_pydcs(b)) + for a, b in _WEAPON_FALLBACKS)) + + +WEAPON_INTRODUCTION_YEARS = { + Weapon.from_pydcs(Weapons.AIM_120C): 1996, + Weapon.from_pydcs(Weapons.AIM_120B): 1994, + Weapon.from_pydcs(Weapons.AIM_7MH): 1987, + Weapon.from_pydcs(Weapons.AIM_7M): 1982, + Weapon.from_pydcs(Weapons.AIM_7F): 1976, + Weapon.from_pydcs(Weapons.AIM_7E): 1963, + Weapon.from_pydcs(Weapons.AIM_9X_Sidewinder_IR_AAM): 2003, + Weapon.from_pydcs(Weapons.AIM_9P5_Sidewinder_IR_AAM): 1963, + Weapon.from_pydcs(Weapons.AIM_9P_Sidewinder_IR_AAM): 1978, + Weapon.from_pydcs(Weapons.AIM_9M_Sidewinder_IR_AAM): 1983, + Weapon.from_pydcs(Weapons.AIM_9L_Sidewinder_IR_AAM): 1977, +} diff --git a/game/settings.py b/game/settings.py index 42e23761..5c6f1a4b 100644 --- a/game/settings.py +++ b/game/settings.py @@ -28,6 +28,7 @@ class Settings: automate_runway_repair: bool = False automate_front_line_reinforcements: bool = False automate_aircraft_reinforcements: bool = False + restrict_weapons_by_date: bool = False # Performance oriented perf_red_alert_state: bool = True diff --git a/gen/aircraft.py b/gen/aircraft.py index 78fba19e..e0fde1d0 100644 --- a/gen/aircraft.py +++ b/gen/aircraft.py @@ -74,7 +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.data.weapons import Pylon, Weapon from game.factions.faction import Faction from game.settings import Settings from game.theater.controlpoint import ( @@ -918,6 +918,24 @@ class AircraftConflictGenerator: 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) + def clear_parking_slots(self) -> None: for cp in self.game.theater.controlpoints: for parking_slot in cp.parking_slots: @@ -1314,6 +1332,8 @@ class AircraftConflictGenerator: # 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: diff --git a/qt_ui/windows/mission/flight/payload/QLoadoutEditor.py b/qt_ui/windows/mission/flight/payload/QLoadoutEditor.py index 35617927..7d017e40 100644 --- a/qt_ui/windows/mission/flight/payload/QLoadoutEditor.py +++ b/qt_ui/windows/mission/flight/payload/QLoadoutEditor.py @@ -1,15 +1,21 @@ -import inspect - -from PySide2.QtWidgets import QLabel, QHBoxLayout, QGroupBox, QSpinBox, QGridLayout, QVBoxLayout, QSizePolicy +from PySide2.QtWidgets import ( + QGridLayout, + QGroupBox, + QLabel, + QSizePolicy, + QVBoxLayout, +) +from game import Game from game.data.weapons import Pylon +from gen.flights.flight import Flight from qt_ui.windows.mission.flight.payload.QPylonEditor import QPylonEditor class QLoadoutEditor(QGroupBox): - def __init__(self, flight, game): - super(QLoadoutEditor, self).__init__("Use custom loadout") + def __init__(self, flight: Flight, game: Game) -> None: + super().__init__("Use custom loadout") self.flight = flight self.game = game self.setCheckable(True) @@ -25,7 +31,7 @@ class QLoadoutEditor(QGroupBox): label.setSizePolicy( QSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)) layout.addWidget(label, i, 0) - layout.addWidget(QPylonEditor(flight, pylon), i, 1) + layout.addWidget(QPylonEditor(game, 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 449229cb..0c388a03 100644 --- a/qt_ui/windows/mission/flight/payload/QPylonEditor.py +++ b/qt_ui/windows/mission/flight/payload/QPylonEditor.py @@ -4,13 +4,14 @@ from typing import Optional from PySide2.QtWidgets import QComboBox +from game import Game from game.data.weapons import Pylon, Weapon from gen.flights.flight import Flight class QPylonEditor(QComboBox): - def __init__(self, flight: Flight, pylon: Pylon) -> None: + def __init__(self, game: Game, flight: Flight, pylon: Pylon) -> None: super().__init__() self.flight = flight self.pylon = pylon @@ -18,7 +19,11 @@ class QPylonEditor(QComboBox): current = self.flight.loadout.get(self.pylon.number) self.addItem("None", None) - allowed = sorted(pylon.allowed, key=operator.attrgetter("name")) + if game.settings.restrict_weapons_by_date: + weapons = pylon.available_on(game.date) + else: + weapons = pylon.allowed + allowed = sorted(weapons, key=operator.attrgetter("name")) for i, weapon in enumerate(allowed): self.addItem(weapon.name, weapon) if current == weapon: diff --git a/qt_ui/windows/settings/QSettingsWindow.py b/qt_ui/windows/settings/QSettingsWindow.py index fa0d49b4..cd6deb4b 100644 --- a/qt_ui/windows/settings/QSettingsWindow.py +++ b/qt_ui/windows/settings/QSettingsWindow.py @@ -248,6 +248,30 @@ class QSettingsWindow(QDialog): campaign_layout.setAlignment(Qt.AlignTop) self.campaign_management_page.setLayout(campaign_layout) + general = QGroupBox("General") + campaign_layout.addWidget(general) + + general_layout = QGridLayout() + general.setLayout(general_layout) + + def set_restict_weapons_by_date(value: bool) -> None: + self.game.settings.restrict_weapons_by_date = value + + restrict_weapons = QCheckBox() + restrict_weapons.setChecked(self.game.settings.restrict_weapons_by_date) + restrict_weapons.toggled.connect(set_restict_weapons_by_date) + + tooltip_text = ( + "Restricts weapon availability based on the campaign date. Data is " + "extremely incomplete so does not affect all weapons." + ) + restrict_weapons.setToolTip(tooltip_text) + restrict_weapons_label = QLabel("Restrict weapons by date (WIP)") + restrict_weapons_label.setToolTip(tooltip_text) + + general_layout.addWidget(restrict_weapons_label, 0, 0) + general_layout.addWidget(restrict_weapons, 0, 1, Qt.AlignRight) + automation = QGroupBox("HQ Automation") campaign_layout.addWidget(automation)