diff --git a/changelog.md b/changelog.md index 7722fcd7..ba02af20 100644 --- a/changelog.md +++ b/changelog.md @@ -5,6 +5,7 @@ Saves from 3.x are not compatible with 5.0. ## Features/Improvements * **[Campaign]** Weapon data such as fallbacks and introduction years is now moddable. Due to the new architecture to support this, the old data was not automatically migrated. +* **[Campaign]** Era-restricted loadouts will now skip LGBs when no TGP is available in the loadout. This only applies to default loadouts; buddy-lasing can be coordinated with custom loadouts. * **[Campaign AI]** Overhauled campaign AI target prioritization. This currently only affects the ordering of DEAD missions. * **[Campaign AI]** Player front line stances can now be automated. Improved stance selection for AI. * **[Campaign AI]** Reworked layout of hold, join, split, and ingress points. Should result in much shorter flight plans in general while still maintaining safe join/split/hold points. diff --git a/game/data/weapons.py b/game/data/weapons.py index 6f2889ec..8e7c86c9 100644 --- a/game/data/weapons.py +++ b/game/data/weapons.py @@ -4,6 +4,7 @@ import datetime import inspect import logging from dataclasses import dataclass, field +from enum import unique, Enum from functools import cached_property from pathlib import Path from typing import Iterator, Optional, Any, ClassVar @@ -61,7 +62,7 @@ class Weapon: duplicate = cls._by_clsid[weapon.clsid] raise ValueError( "Weapon CLSID used in more than one weapon type: " - f"{duplicate.name} and {weapon.name}" + f"{duplicate.name} and {weapon.name}: {weapon.clsid}" ) cls._by_clsid[weapon.clsid] = weapon @@ -91,6 +92,13 @@ class Weapon: fallback = fallback.fallback +@unique +class WeaponType(Enum): + LGB = "LGB" + TGP = "TGP" + UNKNOWN = "unknown" + + @dataclass(frozen=True) class WeaponGroup: """Group of "identical" weapons loaded from resources/weapons. @@ -101,7 +109,10 @@ class WeaponGroup: """ #: The name of the weapon group in the resource file. - name: str = field(compare=False) + name: str + + #: The type of the weapon group. + type: WeaponType = field(compare=False) #: The year of introduction. introduction_year: Optional[int] = field(compare=False) @@ -152,9 +163,13 @@ class WeaponGroup: with group_file_path.open(encoding="utf8") as group_file: data = yaml.safe_load(group_file) name = data["name"] + try: + weapon_type = WeaponType(data["type"]) + except KeyError: + weapon_type = WeaponType.UNKNOWN year = data.get("year") fallback_name = data.get("fallback") - group = WeaponGroup(name, year, fallback_name) + group = WeaponGroup(name, weapon_type, year, fallback_name) for clsid in data["clsids"]: weapon = Weapon(clsid, group) Weapon.register(weapon) @@ -163,7 +178,12 @@ class WeaponGroup: @classmethod def register_clean_pylon(cls) -> None: - group = WeaponGroup("Clean pylon", introduction_year=None, fallback_name=None) + group = WeaponGroup( + "Clean pylon", + type=WeaponType.UNKNOWN, + introduction_year=None, + fallback_name=None, + ) cls.register(group) weapon = Weapon("", group) Weapon.register(weapon) @@ -172,7 +192,12 @@ class WeaponGroup: @classmethod def register_unknown_weapons(cls, seen_clsids: set[str]) -> None: unknown_weapons = set(weapon_ids.keys()) - seen_clsids - group = WeaponGroup("Unknown", introduction_year=None, fallback_name=None) + group = WeaponGroup( + "Unknown", + type=WeaponType.UNKNOWN, + introduction_year=None, + fallback_name=None, + ) cls.register(group) for clsid in unknown_weapons: weapon = Weapon(clsid, group) diff --git a/gen/flights/loadouts.py b/gen/flights/loadouts.py index 826cc01a..0e3dd4d6 100644 --- a/gen/flights/loadouts.py +++ b/gen/flights/loadouts.py @@ -1,9 +1,10 @@ from __future__ import annotations import datetime -from typing import Optional, List, Iterator, TYPE_CHECKING, Mapping +from collections import Iterable +from typing import Optional, Iterator, TYPE_CHECKING, Mapping -from game.data.weapons import Weapon, Pylon +from game.data.weapons import Weapon, Pylon, WeaponType from game.dcs.aircrafttype import AircraftType if TYPE_CHECKING: @@ -30,9 +31,28 @@ class Loadout: def derive_custom(self, name: str) -> Loadout: return Loadout(name, self.pylons, self.date, is_custom=True) + @staticmethod + def _fallback_for( + weapon: Weapon, + pylon: Pylon, + date: datetime.date, + skip_types: Optional[Iterable[WeaponType]] = None, + ) -> Optional[Weapon]: + if skip_types is None: + skip_types = set() + for fallback in weapon.fallbacks: + if not pylon.can_equip(fallback): + continue + if not fallback.available_on(date): + continue + if fallback.weapon_group.type in skip_types: + continue + return fallback + return None + def degrade_for_date(self, unit_type: AircraftType, date: datetime.date) -> Loadout: if self.date is not None and self.date <= date: - return Loadout(self.name, self.pylons, self.date) + return Loadout(self.name, self.pylons, self.date, self.is_custom) new_pylons = dict(self.pylons) for pylon_number, weapon in self.pylons.items(): @@ -41,16 +61,41 @@ class Loadout: continue 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 - else: + fallback = self._fallback_for(weapon, pylon, date) + if fallback is None: del new_pylons[pylon_number] - return Loadout(f"{self.name} ({date.year})", new_pylons, date) + else: + new_pylons[pylon_number] = fallback + loadout = Loadout(self.name, new_pylons, date, self.is_custom) + # If this is not a custom loadout, we should replace any LGBs with iron bombs if + # the loadout lost its TGP. + # + # If the loadout was chosen explicitly by the user, assume they know what + # they're doing. They may be coordinating buddy-lase. + if not loadout.is_custom: + loadout.replace_lgbs_if_no_tgp(unit_type, date) + return loadout + + def replace_lgbs_if_no_tgp( + self, unit_type: AircraftType, date: datetime.date + ) -> None: + for weapon in self.pylons.values(): + if weapon is not None and weapon.weapon_group.type is WeaponType.TGP: + # Have a TGP. Nothing to do. + return + + new_pylons = dict(self.pylons) + for pylon_number, weapon in self.pylons.items(): + if weapon is not None and weapon.weapon_group.type is WeaponType.LGB: + pylon = Pylon.for_aircraft(unit_type, pylon_number) + fallback = self._fallback_for( + weapon, pylon, date, skip_types={WeaponType.LGB} + ) + if fallback is None: + del new_pylons[pylon_number] + else: + new_pylons[pylon_number] = fallback + self.pylons = new_pylons @classmethod def iter_for(cls, flight: Flight) -> Iterator[Loadout]: @@ -72,10 +117,6 @@ class Loadout: 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 diff --git a/resources/weapons/bombs/GBU-10-2X.yaml b/resources/weapons/bombs/GBU-10-2X.yaml index 0f261926..c0c51345 100644 --- a/resources/weapons/bombs/GBU-10-2X.yaml +++ b/resources/weapons/bombs/GBU-10-2X.yaml @@ -1,4 +1,6 @@ name: 2xGBU-10 +type: LGB year: 1976 +fallback: 2xMk 84 clsids: - "{62BE78B1-9258-48AE-B882-279534C0D278}" diff --git a/resources/weapons/bombs/GBU-10.yaml b/resources/weapons/bombs/GBU-10.yaml index 36e30965..4b7306d1 100644 --- a/resources/weapons/bombs/GBU-10.yaml +++ b/resources/weapons/bombs/GBU-10.yaml @@ -1,5 +1,7 @@ name: GBU-10 +type: LGB year: 1976 +fallback: Mk 84 clsids: - "DIS_GBU_10" - "{BRU-32 GBU-10}" diff --git a/resources/weapons/bombs/GBU-12-2X.yaml b/resources/weapons/bombs/GBU-12-2X.yaml index 282667c7..2cd83f89 100644 --- a/resources/weapons/bombs/GBU-12-2X.yaml +++ b/resources/weapons/bombs/GBU-12-2X.yaml @@ -1,5 +1,7 @@ name: 2xGBU-12 +type: LGB year: 1976 +fallback: 2xMk 82 clsids: - "{M2KC_RAFAUT_GBU12}" - "{BRU33_2X_GBU-12}" diff --git a/resources/weapons/bombs/GBU-12.yaml b/resources/weapons/bombs/GBU-12.yaml index 3e9500b3..85705c79 100644 --- a/resources/weapons/bombs/GBU-12.yaml +++ b/resources/weapons/bombs/GBU-12.yaml @@ -1,5 +1,7 @@ name: GBU-12 +type: LGB year: 1976 +fallback: Mk 82 clsids: - "DIS_GBU_12" - "{BRU-32 GBU-12}" diff --git a/resources/weapons/bombs/GBU-16-2X.yaml b/resources/weapons/bombs/GBU-16-2X.yaml index 22a48d70..19afd987 100644 --- a/resources/weapons/bombs/GBU-16-2X.yaml +++ b/resources/weapons/bombs/GBU-16-2X.yaml @@ -1,5 +1,7 @@ name: 2xGBU-16 +type: LGB year: 1976 +fallback: 2xMk 83 clsids: - "{BRU33_2X_GBU-16}" - "{BRU-42_2*GBU-16_LEFT}" diff --git a/resources/weapons/bombs/GBU-16.yaml b/resources/weapons/bombs/GBU-16.yaml index c31f360e..c966af60 100644 --- a/resources/weapons/bombs/GBU-16.yaml +++ b/resources/weapons/bombs/GBU-16.yaml @@ -1,5 +1,7 @@ name: GBU-16 +type: LGB year: 1976 +fallback: Mk 83 clsids: - "DIS_GBU_16" - "{BRU-32 GBU-16}" diff --git a/resources/weapons/bombs/GBU-24.yaml b/resources/weapons/bombs/GBU-24.yaml index b9c8fd14..6258584f 100644 --- a/resources/weapons/bombs/GBU-24.yaml +++ b/resources/weapons/bombs/GBU-24.yaml @@ -1,4 +1,5 @@ name: GBU-24 +type: LGB year: 1986 fallback: GBU-10 clsids: diff --git a/resources/weapons/bombs/Mk-82-2X.yaml b/resources/weapons/bombs/Mk-82-2X.yaml new file mode 100644 index 00000000..3a3d7ea7 --- /dev/null +++ b/resources/weapons/bombs/Mk-82-2X.yaml @@ -0,0 +1,18 @@ +name: 2xMk 82 +fallback: Mk 82 +clsids: + - "{M2KC_RAFAUT_MK82}" + - "{BRU33_2X_MK-82}" + - "DIS_MK_82_DUAL_GDJ_II19_L" + - "DIS_MK_82_DUAL_GDJ_II19_R" + - "{D5D51E24-348C-4702-96AF-97A714E72697}" + - "{TER_9A_2L*MK-82}" + - "{TER_9A_2R*MK-82}" + - "{BRU-42_2*Mk-82_LEFT}" + - "{BRU-42_2*Mk-82_RIGHT}" + - "{BRU42_2*MK82 RS}" + - "{BRU3242_2*MK82 RS}" + - "{PHXBRU3242_2*MK82 RS}" + - "{BRU42_2*MK82 LS}" + - "{BRU3242_2*MK82 LS}" + - "{PHXBRU3242_2*MK82 LS}" diff --git a/resources/weapons/bombs/Mk-82.yaml b/resources/weapons/bombs/Mk-82.yaml new file mode 100644 index 00000000..70733a5b --- /dev/null +++ b/resources/weapons/bombs/Mk-82.yaml @@ -0,0 +1,11 @@ +name: Mk 82 +clsids: + - "{BRU-32 MK-82}" + - "{Mk_82B}" + - "{Mk_82BT}" + - "{Mk_82P}" + - "{Mk_82PT}" + - "{Mk_82SB}" + - "{Mk_82SP}" + - "{Mk_82YT}" + - "{BCE4E030-38E9-423E-98ED-24BE3DA87C32}" diff --git a/resources/weapons/bombs/Mk-83-2X.yaml b/resources/weapons/bombs/Mk-83-2X.yaml new file mode 100644 index 00000000..4d2876ad --- /dev/null +++ b/resources/weapons/bombs/Mk-83-2X.yaml @@ -0,0 +1,7 @@ +name: 2xMk 83 +fallback: Mk 83 +clsids: + - "{BRU33_2X_MK-83}" + - "{18617C93-78E7-4359-A8CE-D754103EDF63}" + - "{BRU-42_2*Mk-83_LEFT}" + - "{BRU-42_2*Mk-83_RIGHT}" diff --git a/resources/weapons/bombs/Mk-83.yaml b/resources/weapons/bombs/Mk-83.yaml new file mode 100644 index 00000000..6df0f06e --- /dev/null +++ b/resources/weapons/bombs/Mk-83.yaml @@ -0,0 +1,16 @@ +name: Mk 83 +clsids: + - "{MAK79_MK83 1R}" + - "{MAK79_MK83 1L}" + - "{BRU-32 MK-83}" + - "{Mk_83BT}" + - "{Mk_83CT}" + - "{Mk_83P}" + - "{Mk_83PT}" + - "{BRU42_MK83 RS}" + - "{BRU3242_MK83 RS}" + - "{PHXBRU3242_MK83 RS}" + - "{7A44FF09-527C-4B7E-B42B-3F111CFE50FB}" + - "{BRU42_MK83 LS}" + - "{BRU3242_MK83 LS}" + - "{PHXBRU3242_MK83 LS}" diff --git a/resources/weapons/pods/atflir.yaml b/resources/weapons/pods/atflir.yaml index 3733a299..a33ee9ca 100644 --- a/resources/weapons/pods/atflir.yaml +++ b/resources/weapons/pods/atflir.yaml @@ -1,4 +1,5 @@ name: AN/ASQ-228 ATFLIR +type: TGP year: 2003 # A bit of a hack, but fixes the common case where the Hornet cheek station is # empty because no TGP is available. diff --git a/resources/weapons/pods/lantirn.yaml b/resources/weapons/pods/lantirn.yaml new file mode 100644 index 00000000..c9af761c --- /dev/null +++ b/resources/weapons/pods/lantirn.yaml @@ -0,0 +1,7 @@ +name: AN/AAQ-14 LANTIRN +type: TGP +year: 1990 +clsids: + - "{F14-LANTIRN-TP}" + - "{CAAC1CFD-6745-416B-AFA4-CB57414856D0}" + - "{D1744B93-2A8A-4C4D-B004-7A09CD8C8F3F}" diff --git a/resources/weapons/pods/litening.yaml b/resources/weapons/pods/litening.yaml index e6fd5141..4bee3ed6 100644 --- a/resources/weapons/pods/litening.yaml +++ b/resources/weapons/pods/litening.yaml @@ -1,4 +1,5 @@ name: AN/AAQ-28 LITENING +type: TGP year: 1999 # A bit of a hack, but fixes the common case where the Hornet cheek station is # empty because no TGP is available. For the Viper this will have no effect