Allow in-line definitions of campaign factions.

A lot of campaigns want to define custom factions. This allows them to
do so without us having to fill the built-in factions list with a bunch
of campaign-specific factions. It also makes custom campaigns more
portable as they don't need to also distribute the custom faction files.
This commit is contained in:
Dan Albert 2023-04-17 23:27:59 -07:00
parent dca256364a
commit 1ac36d03da
11 changed files with 288 additions and 197 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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: <p>In this fictional scenario, a US/Israeli coalition must push north from the Israeli border, through Syria and Lebanon to Aleppo.</p><p><strong>Backstory:</strong> 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.</p>
version: "10.1"
recommended_player_faction:
country: Combined Joint Task Forces Blue
name: Israel-USN 2005
authors: Fuzzle
description:
<p>A joint US Navy/Israeli modern faction for use with the Operation Allied
Sword scenario.</p>
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:
<p>Syria-Lebanon alliance in a modern setting with several imported Russian
assets. Designed for use with the Allied Sword scenario.</p>
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:
<p>In this fictional scenario, a US/Israeli coalition must push north from the
Israeli border, through Syria and Lebanon to
Aleppo.</p><p><strong>Backstory:</strong> 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.</p>
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
- Mi-8MTV2 Hip

View File

@ -1,88 +0,0 @@
---
country: Combined Joint Task Forces Blue
name: Israel-USN 2005 (Allied Sword)
authors: Fuzzle
description:
<p>A joint US Navy/Israeli modern faction for use with the Operation
Allied Sword scenario.</p>
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

View File

@ -1,78 +0,0 @@
---
country: Combined Joint Task Forces Red
name: Syria-Lebanon 2005 (Allied Sword)
authors: Fuzzle
description:
<p>Syria-Lebanon alliance in a modern setting with several imported Russian
assets. Designed for use with the Allied Sword scenario.</p>
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: []

View File

@ -20,6 +20,6 @@ bugs.</p>
{% endif %}
<p><strong>Default factions:</strong></p>
<p><span style="color:#82A466">{{campaign.recommended_player_faction}}</span> VS <span style="color:orange">&nbsp;{{campaign.recommended_enemy_faction}}</span></p>
<p><span style="color:#82A466">{{campaign.recommended_player_faction.name}}</span> VS <span style="color:orange">&nbsp;{{campaign.recommended_enemy_faction.name}}</span></p>
{{ campaign.description|safe }}

View File

@ -1,7 +1,7 @@
<strong>Auteur(s) : {{ campaign.authors }}</strong>
<strong>Factions par défaut :</strong>
<span style="color:#82A466">&nbsp;{{campaign.recommended_player_faction}}</span> VS <span style="color:orange">&nbsp;{{campaign.recommended_enemy_faction}}</span>
<span style="color:#82A466">&nbsp;{{campaign.recommended_player_faction.name}}</span> VS <span style="color:orange">&nbsp;{{campaign.recommended_enemy_faction.name}}</span>
<br/>
{{ campaign.description|safe }}