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]** FOBs generate only $10M per turn (previously $20M like airbases).
* **[Economy]** Carriers and off-map spawns generate no income (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. * **[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 ## Fixes

View File

@ -1,11 +1,15 @@
from __future__ import annotations from __future__ import annotations
import datetime
import inspect import inspect
import logging
from collections import defaultdict
from dataclasses import dataclass 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.unitgroup import FlyingGroup
from dcs.unittype import FlyingType from dcs.unittype import FlyingType
from dcs.weapons_data import Weapons, weapon_ids
PydcsWeapon = Dict[str, Union[int, str]] PydcsWeapon = Dict[str, Union[int, str]]
@ -20,6 +24,14 @@ class Weapon:
name: str name: str
weight: int 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 @property
def as_pydcs(self) -> PydcsWeapon: def as_pydcs(self) -> PydcsWeapon:
return { return {
@ -28,6 +40,13 @@ class Weapon:
"weight": self.weight, "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 @classmethod
def from_pydcs(cls, weapon_data: PydcsWeapon) -> Weapon: def from_pydcs(cls, weapon_data: PydcsWeapon) -> Weapon:
return cls( return cls(
@ -36,6 +55,13 @@ class Weapon:
cast(int, weapon_data["weight"]) 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) @dataclass(frozen=True)
class Pylon: class Pylon:
@ -53,6 +79,11 @@ class Pylon:
def make_pydcs_assignment(self, weapon: Weapon) -> PydcsWeaponAssignment: def make_pydcs_assignment(self, weapon: Weapon) -> PydcsWeaponAssignment:
return self.number, weapon.as_pydcs 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 @classmethod
def for_aircraft(cls, aircraft: Type[FlyingType], number: int) -> Pylon: def for_aircraft(cls, aircraft: Type[FlyingType], number: int) -> Pylon:
# In pydcs these are all arbitrary inner classes of the aircraft type. # 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]: def iter_pylons(cls, aircraft: Type[FlyingType]) -> Iterator[Pylon]:
for pylon in sorted(list(aircraft.pylons)): for pylon in sorted(list(aircraft.pylons)):
yield cls.for_aircraft(aircraft, pylon) 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_runway_repair: bool = False
automate_front_line_reinforcements: bool = False automate_front_line_reinforcements: bool = False
automate_aircraft_reinforcements: bool = False automate_aircraft_reinforcements: bool = False
restrict_weapons_by_date: bool = False
# Performance oriented # Performance oriented
perf_red_alert_state: bool = True perf_red_alert_state: bool = True

View File

@ -74,7 +74,7 @@ from dcs.unittype import FlyingType, UnitType
from game import db from game import db
from game.data.cap_capabilities_db import GUNFIGHTERS 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.factions.faction import Faction
from game.settings import Settings from game.settings import Settings
from game.theater.controlpoint import ( from game.theater.controlpoint import (
@ -918,6 +918,24 @@ class AircraftConflictGenerator:
pylon = Pylon.for_aircraft(flight.unit_type, pylon_number) pylon = Pylon.for_aircraft(flight.unit_type, pylon_number)
pylon.equip(group, weapon) 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: def clear_parking_slots(self) -> None:
for cp in self.game.theater.controlpoints: for cp in self.game.theater.controlpoints:
for parking_slot in cp.parking_slots: for parking_slot in cp.parking_slots:
@ -1314,6 +1332,8 @@ class AircraftConflictGenerator:
# have their TOTs set. # have their TOTs set.
self.flights[-1].waypoints = [takeoff_point] + flight.points self.flights[-1].waypoints = [takeoff_point] + flight.points
self._setup_custom_payload(flight, group) 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, def should_delay_flight(self, flight: Flight,
start_time: timedelta) -> bool: start_time: timedelta) -> bool:

View File

@ -1,15 +1,21 @@
import inspect from PySide2.QtWidgets import (
QGridLayout,
from PySide2.QtWidgets import QLabel, QHBoxLayout, QGroupBox, QSpinBox, QGridLayout, QVBoxLayout, QSizePolicy QGroupBox,
QLabel,
QSizePolicy,
QVBoxLayout,
)
from game import Game
from game.data.weapons import Pylon from game.data.weapons import Pylon
from gen.flights.flight import Flight
from qt_ui.windows.mission.flight.payload.QPylonEditor import QPylonEditor from qt_ui.windows.mission.flight.payload.QPylonEditor import QPylonEditor
class QLoadoutEditor(QGroupBox): class QLoadoutEditor(QGroupBox):
def __init__(self, flight, game): def __init__(self, flight: Flight, game: Game) -> None:
super(QLoadoutEditor, self).__init__("Use custom loadout") super().__init__("Use custom loadout")
self.flight = flight self.flight = flight
self.game = game self.game = game
self.setCheckable(True) self.setCheckable(True)
@ -25,7 +31,7 @@ class QLoadoutEditor(QGroupBox):
label.setSizePolicy( label.setSizePolicy(
QSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)) QSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed))
layout.addWidget(label, i, 0) layout.addWidget(label, i, 0)
layout.addWidget(QPylonEditor(flight, pylon), i, 1) layout.addWidget(QPylonEditor(game, flight, pylon), i, 1)
hboxLayout.addLayout(layout) hboxLayout.addLayout(layout)
hboxLayout.addStretch() hboxLayout.addStretch()

View File

@ -4,13 +4,14 @@ from typing import Optional
from PySide2.QtWidgets import QComboBox from PySide2.QtWidgets import QComboBox
from game import Game
from game.data.weapons import Pylon, Weapon from game.data.weapons import Pylon, Weapon
from gen.flights.flight import Flight from gen.flights.flight import Flight
class QPylonEditor(QComboBox): class QPylonEditor(QComboBox):
def __init__(self, flight: Flight, pylon: Pylon) -> None: def __init__(self, game: Game, flight: Flight, pylon: Pylon) -> None:
super().__init__() super().__init__()
self.flight = flight self.flight = flight
self.pylon = pylon self.pylon = pylon
@ -18,7 +19,11 @@ class QPylonEditor(QComboBox):
current = self.flight.loadout.get(self.pylon.number) current = self.flight.loadout.get(self.pylon.number)
self.addItem("None", None) 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): for i, weapon in enumerate(allowed):
self.addItem(weapon.name, weapon) self.addItem(weapon.name, weapon)
if current == weapon: if current == weapon:

View File

@ -248,6 +248,30 @@ class QSettingsWindow(QDialog):
campaign_layout.setAlignment(Qt.AlignTop) campaign_layout.setAlignment(Qt.AlignTop)
self.campaign_management_page.setLayout(campaign_layout) 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") automation = QGroupBox("HQ Automation")
campaign_layout.addWidget(automation) campaign_layout.addWidget(automation)