diff --git a/changelog.md b/changelog.md index b1570640..399565ee 100644 --- a/changelog.md +++ b/changelog.md @@ -13,6 +13,7 @@ Saves from 6.x are not compatible with 7.0. * **[Modding]** Updated Community A-4E-C mod version support to 2.1.0 release. * **[Modding]** Add support for VSN F-4B and F-4C mod. * **[Modding]** Custom factions can now be defined in YAML as well as JSON. JSON support may be removed in the future if having both formats causes confusion. +* **[Modding]** Campaigns which require custom factions can now define those factions directly in the campaign YAML. See Operation Aliied Sword for an example. ## Fixes diff --git a/game/campaignloader/campaign.py b/game/campaignloader/campaign.py index c61ae2a4..208ead32 100644 --- a/game/campaignloader/campaign.py +++ b/game/campaignloader/campaign.py @@ -5,22 +5,24 @@ import logging from collections.abc import Iterator from dataclasses import dataclass from pathlib import Path -from typing import Any, Dict, Tuple +from typing import Any, Dict, TYPE_CHECKING, Tuple import yaml from packaging.version import Version from game import persistence from game.profiling import logged_duration -from game.theater import ( - ConflictTheater, -) +from game.theater import ConflictTheater from game.theater.iadsnetwork.iadsnetwork import IadsNetwork from game.theater.theaterloader import TheaterLoader from game.version import CAMPAIGN_FORMAT_VERSION from .campaignairwingconfig import CampaignAirWingConfig +from .factionrecommendation import FactionRecommendation from .mizcampaignloader import MizCampaignLoader +if TYPE_CHECKING: + from game.factions.factions import Factions + PERF_FRIENDLY = 0 PERF_MEDIUM = 1 PERF_HARD = 2 @@ -40,8 +42,8 @@ class Campaign: #: selecting a campaign that is not up to date. version: Tuple[int, int] - recommended_player_faction: str - recommended_enemy_faction: str + recommended_player_faction: FactionRecommendation + recommended_enemy_faction: FactionRecommendation recommended_start_date: datetime.date | None recommended_start_time: datetime.time | None @@ -60,7 +62,6 @@ class Campaign: with path.open(encoding="utf-8") as campaign_file: data = yaml.safe_load(campaign_file) - sanitized_theater = data["theater"].replace(" ", "") version_field = data.get("version", "0") try: version = Version(version_field) @@ -93,8 +94,12 @@ class Campaign: data.get("authors", "???"), data.get("description", ""), (version.major, version.minor), - data.get("recommended_player_faction", "USA 2005"), - data.get("recommended_enemy_faction", "Russia 1990"), + FactionRecommendation.from_field( + data.get("recommended_player_faction"), player=True + ), + FactionRecommendation.from_field( + data.get("recommended_enemy_faction"), player=False + ), start_date, start_time, data.get("recommended_player_money", DEFAULT_BUDGET), @@ -163,6 +168,10 @@ class Campaign: return False return True + def register_campaign_specific_factions(self, factions: Factions) -> None: + self.recommended_player_faction.register_campaign_specific_faction(factions) + self.recommended_enemy_faction.register_campaign_specific_faction(factions) + @staticmethod def iter_campaigns_in_dir(path: Path) -> Iterator[Path]: yield from path.glob("*.yaml") diff --git a/game/campaignloader/factionrecommendation.py b/game/campaignloader/factionrecommendation.py new file mode 100644 index 00000000..b9b2d841 --- /dev/null +++ b/game/campaignloader/factionrecommendation.py @@ -0,0 +1,53 @@ +from __future__ import annotations + +from abc import ABC, abstractmethod +from typing import Any, TYPE_CHECKING + +from game.factions import Faction + +if TYPE_CHECKING: + from game.factions.factions import Factions + + +class FactionRecommendation(ABC): + def __init__(self, name: str) -> None: + self.name = name + + @abstractmethod + def register_campaign_specific_faction(self, factions: Factions) -> None: + ... + + @abstractmethod + def get_faction(self, factions: Factions) -> Faction: + ... + + @staticmethod + def from_field( + data: str | dict[str, Any] | None, player: bool + ) -> FactionRecommendation: + if data is None: + name = "USA 2005" if player else "Russia 1990" + return BuiltinFactionRecommendation(name) + if isinstance(data, str): + return BuiltinFactionRecommendation(data) + return CampaignDefinedFactionRecommendation(Faction.from_dict(data)) + + +class BuiltinFactionRecommendation(FactionRecommendation): + def register_campaign_specific_faction(self, factions: Factions) -> None: + pass + + def get_faction(self, factions: Factions) -> Faction: + return factions.get_by_name(self.name) + + +class CampaignDefinedFactionRecommendation(FactionRecommendation): + def __init__(self, faction: Faction) -> None: + super().__init__(faction.name) + self.faction = faction + + def register_campaign_specific_faction(self, factions: Factions) -> None: + factions.add_campaign_defined(self.faction) + + def get_faction(self, factions: Factions) -> Faction: + return self.faction diff --git a/game/factions/factions.py b/game/factions/factions.py index 99ae5449..37f8e836 100644 --- a/game/factions/factions.py +++ b/game/factions/factions.py @@ -1,5 +1,6 @@ from __future__ import annotations +import itertools import json import logging from collections.abc import Iterator @@ -14,12 +15,29 @@ from .faction import Faction class Factions: def __init__(self, factions: dict[str, Faction]) -> None: self.factions = factions + self.campaign_defined_factions: dict[str, Faction] = {} def get_by_name(self, name: str) -> Faction: - return self.factions[name] + try: + return self.factions[name] + except KeyError: + return self.campaign_defined_factions[name] def iter_faction_names(self) -> Iterator[str]: - return iter(self.factions.keys()) + # Campaign defined factions first so they show up at the top of the list in the + # UI. + return itertools.chain(self.campaign_defined_factions, self.factions) + + def add_campaign_defined(self, faction: Faction) -> None: + if ( + faction.name in self.factions + or faction.name in self.campaign_defined_factions + ): + raise KeyError(f"Duplicate faction {faction.name}") + self.campaign_defined_factions[faction.name] = faction + + def reset_campaign_defined(self) -> None: + self.campaign_defined_factions = {} @staticmethod def iter_faction_files_in(path: Path) -> Iterator[Path]: diff --git a/game/version.py b/game/version.py index 3a682a9d..3b593664 100644 --- a/game/version.py +++ b/game/version.py @@ -169,4 +169,7 @@ VERSION = _build_version_string() #: #: Version 10.5 #: * Support for scenery objectives defined by quad zones. -CAMPAIGN_FORMAT_VERSION = (10, 5) +#: +#: Version 10.6 +#: * Support in-line definitions of campaign-specific factions. +CAMPAIGN_FORMAT_VERSION = (10, 6) diff --git a/qt_ui/windows/newgame/QNewGameWizard.py b/qt_ui/windows/newgame/QNewGameWizard.py index d6a17732..3c302b96 100644 --- a/qt_ui/windows/newgame/QNewGameWizard.py +++ b/qt_ui/windows/newgame/QNewGameWizard.py @@ -83,9 +83,11 @@ class NewGameWizard(QtWidgets.QWizard): def __init__(self, parent=None): super(NewGameWizard, self).__init__(parent) + factions = Factions.load() + self.campaigns = list(sorted(Campaign.load_each(), key=lambda x: x.name)) - self.faction_selection_page = FactionSelection() + self.faction_selection_page = FactionSelection(factions) self.addPage(IntroPage()) self.theater_page = TheaterConfiguration( self.campaigns, self.faction_selection_page @@ -222,10 +224,10 @@ class IntroPage(QtWidgets.QWizardPage): class FactionSelection(QtWidgets.QWizardPage): - def __init__(self, parent=None): - super(FactionSelection, self).__init__(parent) + def __init__(self, factions: Factions, parent=None) -> None: + super().__init__(parent) - self.factions = Factions.load() + self.factions = factions self.setTitle("Faction selection") self.setSubTitle( @@ -308,15 +310,15 @@ class FactionSelection(QtWidgets.QWizardPage): self.blueFactionSelect.clear() self.redFactionSelect.clear() - for f in self.factions.iter_faction_names(): - self.blueFactionSelect.addItem(f) + self.factions.reset_campaign_defined() + campaign.register_campaign_specific_factions(self.factions) - for i, r in enumerate(self.factions.iter_faction_names()): - self.redFactionSelect.addItem(r) - if r == campaign.recommended_enemy_faction: - self.redFactionSelect.setCurrentIndex(i) - if r == campaign.recommended_player_faction: - self.blueFactionSelect.setCurrentIndex(i) + for name in self.factions.iter_faction_names(): + self.blueFactionSelect.addItem(name) + self.redFactionSelect.addItem(name) + + self.blueFactionSelect.setCurrentText(campaign.recommended_player_faction.name) + self.redFactionSelect.setCurrentText(campaign.recommended_enemy_faction.name) self.updateUnitRecap() diff --git a/resources/campaigns/operation_allied_sword.yaml b/resources/campaigns/operation_allied_sword.yaml index 7a269916..1d0a570d 100644 --- a/resources/campaigns/operation_allied_sword.yaml +++ b/resources/campaigns/operation_allied_sword.yaml @@ -2,10 +2,181 @@ name: Syria - Operation Allied Sword theater: Syria authors: Fuzzle -recommended_player_faction: Israel-USN 2005 (Allied Sword) -recommended_enemy_faction: Syria-Lebanon 2005 (Allied Sword) -description:

In this fictional scenario, a US/Israeli coalition must push north from the Israeli border, through Syria and Lebanon to Aleppo.

Backstory: A Syrian-Lebanese joint force (with Russian materiel support) has attacked Israel, attmepting to cross the northern border. With the arrival of a US carrier group, Israel prepares its counterattack. The US Navy will handle the Beirut region's coastal arena, while the IAF will push through Damascus and the inland mountain ranges.

-version: "10.1" +recommended_player_faction: + country: Combined Joint Task Forces Blue + name: Israel-USN 2005 + authors: Fuzzle + description: +

A joint US Navy/Israeli modern faction for use with the Operation Allied + Sword scenario.

+ locales: + - en_US + aircrafts: + - F-4E Phantom II + - F-15C Eagle + - F-15E Strike Eagle + - F-16CM Fighting Falcon (Block 50) + - F-14B Tomcat + - F/A-18C Hornet (Lot 20) + - AV-8B Harrier II Night Attack + - AH-1W SuperCobra + - AH-64D Apache Longbow + - AH-64D Apache Longbow (AI) + - S-3B Viking + - SH-60B Seahawk + - UH-1H Iroquois + - UH-60L + awacs: + - E-2C Hawkeye + tankers: + - KC-130 + - S-3B Tanker + frontline_units: + - M113 + - M1043 HMMWV (M2 HMG) + - M1045 HMMWV (BGM-71 TOW) + - Merkava Mk IV + - M163 Vulcan Air Defense System + artillery_units: + - M109A6 Paladin + - M270 Multiple Launch Rocket System + logistics_units: + - Truck M818 6x6 + infantry_units: + - Infantry M4 + - Infantry M249 + - MANPADS Stinger + preset_groups: + - Hawk + - Patriot + naval_units: + - FFG Oliver Hazard Perry + - DDG Arleigh Burke IIa + - CG Ticonderoga + - LHA-1 Tarawa + - CVN-74 John C. Stennis + missiles: [] + air_defense_units: + - SAM Hawk SR (AN/MPQ-50) + - M163 Vulcan Air Defense System + - M48 Chaparral + requirements: {} + carrier_names: + - CVN-71 Theodore Roosevelt + - CVN-72 Abraham Lincoln + - CVN-73 George Washington + - CVN-74 John C. Stennis + - CVN-75 Harry S. Truman + helicopter_carrier_names: + - LHA-1 Tarawa + - LHA-2 Saipan + - LHA-3 Belleau Wood + - LHA-4 Nassau + - LHA-5 Peleliu + has_jtac: true + jtac_unit: MQ-9 Reaper + doctrine: modern + liveries_overrides: + F-14B Tomcat: + - VF-142 Ghostriders + F/A-18C Hornet (Lot 20): + - VMFA-251 high visibility + AV-8B Harrier II Night Attack: + - VMAT-542 + AH-1W SuperCobra: + - Marines + UH-1H Iroquois: + - US NAVY + UH-60L: + - Israeli Air Force + unrestricted_satnav: true +recommended_enemy_faction: + country: Combined Joint Task Forces Red + name: Syria-Lebanon 2005 (Allied Sword) + authors: Fuzzle + description: +

Syria-Lebanon alliance in a modern setting with several imported Russian + assets. Designed for use with the Allied Sword scenario.

+ aircrafts: + - MiG-23ML Flogger-G + - MiG-25RBT Foxbat-B + - MiG-29A Fulcrum-A + - Su-17M4 Fitter-K + - Su-24M Fencer-D + - Su-30 Flanker-C + - Su-34 Fullback + - L-39ZA Albatros + - Tu-22M3 Backfire-C + - Mi-24V Hind-E + - Mi-8MTV2 Hip + - SA 342M Gazelle + - SA 342L Gazelle + - IL-76MD + awacs: + - A-50 + tankers: + - IL-78M + frontline_units: + - BMP-1 + - BMP-2 + - BTR-80 + - BRDM-2 + - MT-LB + - T-55A + - T-72B with Kontakt-1 ERA + - T-90A + - ZSU-57-2 'Sparka' + artillery_units: + - BM-21 Grad + - 2S1 Gvozdika + logistics_units: + - Truck Ural-375 + - LUV UAZ-469 Jeep + infantry_units: + - Paratrooper AKS + - Infantry AK-74 Rus + - Paratrooper RPG-16 + - MANPADS SA-18 Igla-S "Grouse" + preset_groups: + - SA-2/S-75 + - SA-3/S-125 + - SA-6 + - SA-11 + - SA-10/S-300PS + - Silkworm + - Cold-War-Flak + - Russian Navy + naval_units: + - Corvette 1124.4 Grish + - Corvette 1241.1 Molniya + - FAC La Combattante IIa + - Frigate 1135M Rezky + air_defense_units: + - SAM P19 "Flat Face" SR (SA-2/3) + - EWR 1L13 + - EWR 55G6 + - SAM SA-8 Osa "Gecko" TEL + - SA-9 Strela + - SA-13 Gopher (9K35 Strela-10M3) + - SA-19 Grison (2K22 Tunguska) + - ZSU-57-2 'Sparka' + - AAA ZU-23 Closed Emplacement + - ZU-23 on Ural-375 + - ZSU-23-4 Shilka + missiles: + - SSM SS-1C Scud-B + helicopter_carrier_names: [] + requirements: {} + carrier_names: [] +description: +

In this fictional scenario, a US/Israeli coalition must push north from the + Israeli border, through Syria and Lebanon to + Aleppo.

Backstory: A Syrian-Lebanese joint force (with + Russian materiel support) has attacked Israel, attmepting to cross the + northern border. With the arrival of a US carrier group, Israel prepares its + counterattack. The US Navy will handle the Beirut region's coastal arena, + while the IAF will push through Damascus and the inland mountain ranges.

+version: "10.6" miz: operation_allied_sword.miz performance: 2 recommended_start_date: 2004-07-17 @@ -217,4 +388,4 @@ squadrons: - primary: Transport secondary: air-to-ground aircraft: - - Mi-8MTV2 Hip \ No newline at end of file + - Mi-8MTV2 Hip diff --git a/resources/factions/Israel-USN_2005_Allied_Sword.yaml b/resources/factions/Israel-USN_2005_Allied_Sword.yaml deleted file mode 100644 index 7516c0d9..00000000 --- a/resources/factions/Israel-USN_2005_Allied_Sword.yaml +++ /dev/null @@ -1,88 +0,0 @@ ---- -country: Combined Joint Task Forces Blue -name: Israel-USN 2005 (Allied Sword) -authors: Fuzzle -description: -

A joint US Navy/Israeli modern faction for use with the Operation - Allied Sword scenario.

-locales: - - en_US -aircrafts: - - F-4E Phantom II - - F-15C Eagle - - F-15E Strike Eagle - - F-16CM Fighting Falcon (Block 50) - - F-14B Tomcat - - F/A-18C Hornet (Lot 20) - - AV-8B Harrier II Night Attack - - AH-1W SuperCobra - - AH-64D Apache Longbow - - AH-64D Apache Longbow (AI) - - S-3B Viking - - SH-60B Seahawk - - UH-1H Iroquois - - UH-60L -awacs: - - E-2C Hawkeye -tankers: - - KC-130 - - S-3B Tanker -frontline_units: - - M113 - - M1043 HMMWV (M2 HMG) - - M1045 HMMWV (BGM-71 TOW) - - Merkava Mk IV - - M163 Vulcan Air Defense System -artillery_units: - - M109A6 Paladin - - M270 Multiple Launch Rocket System -logistics_units: - - Truck M818 6x6 -infantry_units: - - Infantry M4 - - Infantry M249 - - MANPADS Stinger -preset_groups: - - Hawk - - Patriot -naval_units: - - FFG Oliver Hazard Perry - - DDG Arleigh Burke IIa - - CG Ticonderoga - - LHA-1 Tarawa - - CVN-74 John C. Stennis -missiles: [] -air_defense_units: - - SAM Hawk SR (AN/MPQ-50) - - M163 Vulcan Air Defense System - - M48 Chaparral -requirements: {} -carrier_names: - - CVN-71 Theodore Roosevelt - - CVN-72 Abraham Lincoln - - CVN-73 George Washington - - CVN-74 John C. Stennis - - CVN-75 Harry S. Truman -helicopter_carrier_names: - - LHA-1 Tarawa - - LHA-2 Saipan - - LHA-3 Belleau Wood - - LHA-4 Nassau - - LHA-5 Peleliu -has_jtac: true -jtac_unit: MQ-9 Reaper -doctrine: modern -liveries_overrides: - F-14B Tomcat: - - VF-142 Ghostriders - F/A-18C Hornet (Lot 20): - - VMFA-251 high visibility - AV-8B Harrier II Night Attack: - - VMAT-542 - AH-1W SuperCobra: - - Marines - UH-1H Iroquois: - - US NAVY - UH-60L: - - Israeli Air Force -unrestricted_satnav: true diff --git a/resources/factions/Syria-Lebanon_2005_Allied_Sword.yaml b/resources/factions/Syria-Lebanon_2005_Allied_Sword.yaml deleted file mode 100644 index 3e0ce5b8..00000000 --- a/resources/factions/Syria-Lebanon_2005_Allied_Sword.yaml +++ /dev/null @@ -1,78 +0,0 @@ ---- -country: Combined Joint Task Forces Red -name: Syria-Lebanon 2005 (Allied Sword) -authors: Fuzzle -description: -

Syria-Lebanon alliance in a modern setting with several imported Russian - assets. Designed for use with the Allied Sword scenario.

-aircrafts: - - MiG-23ML Flogger-G - - MiG-25RBT Foxbat-B - - MiG-29A Fulcrum-A - - Su-17M4 Fitter-K - - Su-24M Fencer-D - - Su-30 Flanker-C - - Su-34 Fullback - - L-39ZA Albatros - - Tu-22M3 Backfire-C - - Mi-24V Hind-E - - Mi-8MTV2 Hip - - SA 342M Gazelle - - SA 342L Gazelle - - IL-76MD -awacs: - - A-50 -tankers: - - IL-78M -frontline_units: - - BMP-1 - - BMP-2 - - BTR-80 - - BRDM-2 - - MT-LB - - T-55A - - T-72B with Kontakt-1 ERA - - T-90A - - ZSU-57-2 'Sparka' -artillery_units: - - BM-21 Grad - - 2S1 Gvozdika -logistics_units: - - Truck Ural-375 - - LUV UAZ-469 Jeep -infantry_units: - - Paratrooper AKS - - Infantry AK-74 Rus - - Paratrooper RPG-16 - - MANPADS SA-18 Igla-S "Grouse" -preset_groups: - - SA-2/S-75 - - SA-3/S-125 - - SA-6 - - SA-11 - - SA-10/S-300PS - - Silkworm - - Cold-War-Flak - - Russian Navy -naval_units: - - Corvette 1124.4 Grish - - Corvette 1241.1 Molniya - - FAC La Combattante IIa - - Frigate 1135M Rezky -air_defense_units: - - SAM P19 "Flat Face" SR (SA-2/3) - - EWR 1L13 - - EWR 55G6 - - SAM SA-8 Osa "Gecko" TEL - - SA-9 Strela - - SA-13 Gopher (9K35 Strela-10M3) - - SA-19 Grison (2K22 Tunguska) - - ZSU-57-2 'Sparka' - - AAA ZU-23 Closed Emplacement - - ZU-23 on Ural-375 - - ZSU-23-4 Shilka -missiles: - - SSM SS-1C Scud-B -helicopter_carrier_names: [] -requirements: {} -carrier_names: [] diff --git a/resources/ui/templates/campaigntemplate_EN.j2 b/resources/ui/templates/campaigntemplate_EN.j2 index c9ee31e2..a5b1a2d4 100644 --- a/resources/ui/templates/campaigntemplate_EN.j2 +++ b/resources/ui/templates/campaigntemplate_EN.j2 @@ -20,6 +20,6 @@ bugs.

{% endif %}

Default factions:

-

{{campaign.recommended_player_faction}} VS  {{campaign.recommended_enemy_faction}}

+

{{campaign.recommended_player_faction.name}} VS  {{campaign.recommended_enemy_faction.name}}

{{ campaign.description|safe }} \ No newline at end of file diff --git a/resources/ui/templates/campaigntemplate_FR.j2 b/resources/ui/templates/campaigntemplate_FR.j2 index 5469160a..ea6111e6 100644 --- a/resources/ui/templates/campaigntemplate_FR.j2 +++ b/resources/ui/templates/campaigntemplate_FR.j2 @@ -1,7 +1,7 @@ Auteur(s) : {{ campaign.authors }} Factions par défaut : - {{campaign.recommended_player_faction}} VS  {{campaign.recommended_enemy_faction}} + {{campaign.recommended_player_faction.name}} VS  {{campaign.recommended_enemy_faction.name}}
{{ campaign.description|safe }}