From 1ac36d03da164238d7dd6f4851efe3941cd8a7f8 Mon Sep 17 00:00:00 2001
From: Dan Albert
Date: Mon, 17 Apr 2023 23:27:59 -0700
Subject: [PATCH] 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.
---
changelog.md | 1 +
game/campaignloader/campaign.py | 27 ++-
game/campaignloader/factionrecommendation.py | 53 +++++
game/factions/factions.py | 22 ++-
game/version.py | 5 +-
qt_ui/windows/newgame/QNewGameWizard.py | 26 +--
.../campaigns/operation_allied_sword.yaml | 181 +++++++++++++++++-
.../Israel-USN_2005_Allied_Sword.yaml | 88 ---------
.../Syria-Lebanon_2005_Allied_Sword.yaml | 78 --------
resources/ui/templates/campaigntemplate_EN.j2 | 2 +-
resources/ui/templates/campaigntemplate_FR.j2 | 2 +-
11 files changed, 288 insertions(+), 197 deletions(-)
create mode 100644 game/campaignloader/factionrecommendation.py
delete mode 100644 resources/factions/Israel-USN_2005_Allied_Sword.yaml
delete mode 100644 resources/factions/Syria-Lebanon_2005_Allied_Sword.yaml
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 }}