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
This commit is contained in:
Dan Albert 2021-01-02 14:38:07 -08:00
parent 507b217065
commit 34945e7eba
7 changed files with 132 additions and 10 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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