diff --git a/changelog.md b/changelog.md index 768ec51e..365a6610 100644 --- a/changelog.md +++ b/changelog.md @@ -44,6 +44,7 @@ * **[Package Planning]** Ability to plan recovery tanker flights * **[Modding]** Support for Bandit's cloud presets mod (v15) * **[UX]** Reduce size of save-file by loading landmap data on the fly, which also implies no new campaign needs to be started to benefit from an updated landmap +* **[New Game Wizard]** Ability to save an edited faction during new game creation ## Fixes * **[UI/UX]** A-10A flights can be edited again diff --git a/game/factions/faction.py b/game/factions/faction.py index a08c8a29..f544788a 100644 --- a/game/factions/faction.py +++ b/game/factions/faction.py @@ -321,6 +321,38 @@ class Faction: return faction + def to_dict(self) -> dict[str, Any]: + return { + "country": self.country.name, + "name": self.name, + "description": self.description, + "authors": self.authors, + "aircrafts": [ac.variant_id for ac in self.aircraft], + "awacs": [ac.variant_id for ac in self.awacs], + "tankers": [ac.variant_id for ac in self.tankers], + "frontline_units": [unit.variant_id for unit in self.frontline_units], + "artillery_units": [unit.variant_id for unit in self.artillery_units], + "logistics_units": [unit.variant_id for unit in self.logistics_units], + "infantry_units": [unit.variant_id for unit in self.infantry_units], + "preset_groups": [group.name for group in self.preset_groups], + "air_defense_units": [unit.variant_id for unit in self.air_defense_units], + "naval_units": [unit.variant_id for unit in self.naval_units], + "missiles": [unit.variant_id for unit in self.missiles], + "has_jtac": self.has_jtac, + "jtac_unit": self.jtac_unit.variant_id if self.jtac_unit else None, + "doctrine": self.doctrine.name, + "building_set": list(self.building_set), + "liveries_overrides": { + ac.variant_id: livery for ac, livery in self.liveries_overrides.items() + }, + "liveries_overrides_ground_forces": self.liveries_overrides_ground_forces, + "unrestricted_satnav": self.unrestricted_satnav, + "requirements": self.requirements, + "carriers": { + carrier.variant_id: names for carrier, names in self.carriers.items() + }, + } + @property def ground_units(self) -> Iterator[GroundUnitType]: yield from self.artillery_units diff --git a/game/factions/factionloader.py b/game/factions/factionloader.py index ac1ccea7..ea4d4dee 100644 --- a/game/factions/factionloader.py +++ b/game/factions/factionloader.py @@ -37,7 +37,7 @@ class FactionLoader: @classmethod def load_factions(cls: Type[FactionLoader]) -> Dict[str, Faction]: - user_faction_path = persistency.base_path() / "Retribution/Factions" + user_faction_path = persistency.factions_dir() files = cls.find_faction_files_in( FACTION_DIRECTORY ) + cls.find_faction_files_in(user_faction_path) diff --git a/game/persistency.py b/game/persistency.py index 6b2dad68..78ed184a 100644 --- a/game/persistency.py +++ b/game/persistency.py @@ -150,6 +150,10 @@ def debug_dir() -> Path: return _create_dir_if_needed(base_path() / "Retribution" / "Debug") +def factions_dir() -> Path: + return _create_dir_if_needed(base_path() / "Retribution" / "Factions") + + def groups_dir() -> Path: return _create_dir_if_needed(base_path() / "Retribution" / "Groups") diff --git a/qt_ui/windows/newgame/WizardPages/QFactionSelection.py b/qt_ui/windows/newgame/WizardPages/QFactionSelection.py index 3209991c..e74a10cd 100644 --- a/qt_ui/windows/newgame/WizardPages/QFactionSelection.py +++ b/qt_ui/windows/newgame/WizardPages/QFactionSelection.py @@ -1,7 +1,9 @@ from __future__ import unicode_literals +import json + from copy import deepcopy -from typing import Union, Callable, Set, Optional +from typing import Union, Callable, Set, Optional, List from PySide6 import QtWidgets, QtGui from PySide6.QtCore import Qt @@ -15,14 +17,22 @@ from PySide6.QtWidgets import ( QPushButton, QComboBox, QHBoxLayout, + QVBoxLayout, + QLineEdit, + QDialog, + QFileDialog, ) +from game import persistency +from game.armedforces.forcegroup import ForceGroup from game.ato import FlightType from game.campaignloader import Campaign from game.dcs.aircrafttype import AircraftType from game.dcs.groundunittype import GroundUnitType +from game.dcs.shipunittype import ShipUnitType from game.dcs.unittype import UnitType from game.factions import Faction, FACTIONS +from qt_ui.uiconstants import EVENT_ICONS from qt_ui.windows.newgame.jinja_env import jinja_env @@ -62,69 +72,128 @@ class QFactionUnits(QScrollArea): self.checkboxes: dict[str, QCheckBox] = {} grid = QGridLayout() grid.setColumnStretch(1, 1) - if len(self.faction.aircraft) > 0: - self.add_ac_combo = QComboBox() - hbox = self._create_aircraft_combobox( - self.add_ac_combo, - lambda: self._on_add_ac(self.faction.aircraft, self.add_ac_combo), - self._aircraft_predicate, - ) - grid.addWidget(QLabel("Aircraft:"), counter, 0) - counter = self._add_checkboxes(self.faction.aircraft, counter, grid, hbox) - if len(self.faction.awacs) > 0: - self.add_awacs_combo = QComboBox() - hbox = self._create_aircraft_combobox( - self.add_awacs_combo, - lambda: self._on_add_ac(self.faction.awacs, self.add_awacs_combo), - self._awacs_predicate, - ) - grid.addWidget(QLabel("AWACS:"), counter, 0) - counter = self._add_checkboxes(self.faction.awacs, counter, grid, hbox) - if len(self.faction.tankers) > 0: - self.add_tanker_combo = QComboBox() - hbox = self._create_aircraft_combobox( - self.add_tanker_combo, - lambda: self._on_add_ac(self.faction.tankers, self.add_tanker_combo), - self._tanker_predicate, - ) - grid.addWidget(QLabel("Tankers:"), counter, 0) - counter = self._add_checkboxes(self.faction.tankers, counter, grid, hbox) - if len(self.faction.frontline_units) > 0: - self.add_frontline_combo = QComboBox() - hbox = self._create_unit_combobox( - self.add_frontline_combo, - lambda: self._on_add_unit( - self.faction.frontline_units, self.add_frontline_combo - ), - self.faction.frontline_units, - ) - grid.addWidget(QLabel("Frontlines vehicles:"), counter, 0) - counter = self._add_checkboxes( - self.faction.frontline_units, counter, grid, hbox - ) - if len(self.faction.artillery_units) > 0: - grid.addWidget(QLabel("Artillery units:"), counter, 0) - counter = self._add_checkboxes(self.faction.artillery_units, counter, grid) - if len(self.faction.logistics_units) > 0: - grid.addWidget(QLabel("Logistics units:"), counter, 0) - counter = self._add_checkboxes(self.faction.logistics_units, counter, grid) - if len(self.faction.infantry_units) > 0: - grid.addWidget(QLabel("Infantry units:"), counter, 0) - counter = self._add_checkboxes(self.faction.infantry_units, counter, grid) - if len(self.faction.preset_groups) > 0: - grid.addWidget(QLabel("Preset groups:"), counter, 0) - counter = self._add_checkboxes(self.faction.preset_groups, counter, grid) - if len(self.faction.air_defense_units) > 0: - grid.addWidget(QLabel("Air defenses:"), counter, 0) - counter = self._add_checkboxes( - self.faction.air_defense_units, counter, grid - ) - if len(self.faction.naval_units) > 0: - grid.addWidget(QLabel("Naval units:"), counter, 0) - counter = self._add_checkboxes(self.faction.naval_units, counter, grid) - if len(self.faction.missiles) > 0: - grid.addWidget(QLabel("Missile units:"), counter, 0) - self._add_checkboxes(self.faction.missiles, counter, grid) + self.add_ac_combo = QComboBox() + hbox = self._create_aircraft_combobox( + self.add_ac_combo, + lambda: self._on_add_ac(self.faction.aircraft, self.add_ac_combo), + self._aircraft_predicate, + ) + grid.addWidget(QLabel("Aircraft:"), counter, 0) + counter = self._add_checkboxes(self.faction.aircraft, counter, grid, hbox) + + self.add_awacs_combo = QComboBox() + hbox = self._create_aircraft_combobox( + self.add_awacs_combo, + lambda: self._on_add_ac(self.faction.awacs, self.add_awacs_combo), + self._awacs_predicate, + ) + grid.addWidget(QLabel("AWACS:"), counter, 0) + counter = self._add_checkboxes(self.faction.awacs, counter, grid, hbox) + + self.add_tanker_combo = QComboBox() + hbox = self._create_aircraft_combobox( + self.add_tanker_combo, + lambda: self._on_add_ac(self.faction.tankers, self.add_tanker_combo), + self._tanker_predicate, + ) + grid.addWidget(QLabel("Tankers:"), counter, 0) + counter = self._add_checkboxes(self.faction.tankers, counter, grid, hbox) + + self.add_frontline_combo = QComboBox() + hbox = self._create_unit_combobox( + self.add_frontline_combo, + lambda: self._on_add_unit( + self.faction.frontline_units, self.add_frontline_combo + ), + self.faction.frontline_units, + ["Frontline vehicles"], + ) + grid.addWidget(QLabel("Frontlines vehicles:"), counter, 0) + counter = self._add_checkboxes( + self.faction.frontline_units, counter, grid, hbox + ) + + self.add_artillery_combo = QComboBox() + hbox = self._create_unit_combobox( + self.add_artillery_combo, + lambda: self._on_add_unit( + self.faction.artillery_units, self.add_artillery_combo + ), + self.faction.artillery_units, + ["Artillery"], + ) + grid.addWidget(QLabel("Artillery units:"), counter, 0) + counter = self._add_checkboxes( + self.faction.artillery_units, counter, grid, hbox + ) + + self.add_logistics_combo = QComboBox() + hbox = self._create_unit_combobox( + self.add_logistics_combo, + lambda: self._on_add_unit( + self.faction.logistics_units, self.add_logistics_combo + ), + self.faction.logistics_units, + ["Logistics"], + ) + grid.addWidget(QLabel("Logistics units:"), counter, 0) + counter = self._add_checkboxes( + self.faction.logistics_units, counter, grid, hbox + ) + + self.add_infantry_combo = QComboBox() + hbox = self._create_unit_combobox( + self.add_infantry_combo, + lambda: self._on_add_unit( + self.faction.infantry_units, self.add_infantry_combo + ), + self.faction.infantry_units, + ["Infantry"], + ) + grid.addWidget(QLabel("Infantry units:"), counter, 0) + counter = self._add_checkboxes(self.faction.infantry_units, counter, grid, hbox) + + self.add_preset_group_combo = QComboBox() + hbox = self._create_preset_group_combobox( + self.add_preset_group_combo, + lambda: self._on_add_preset_group( + self.faction.preset_groups, self.add_preset_group_combo + ), + ) + grid.addWidget(QLabel("Preset groups:"), counter, 0) + counter = self._add_checkboxes(self.faction.preset_groups, counter, grid, hbox) + + self.add_air_defense_combo = QComboBox() + hbox = self._create_unit_combobox( + self.add_air_defense_combo, + lambda: self._on_add_unit( + self.faction.air_defense_units, self.add_air_defense_combo + ), + self.faction.air_defense_units, + ["EarlyWarningRadar", "AAA", "SHORAD"], + ) + grid.addWidget(QLabel("Air defenses:"), counter, 0) + counter = self._add_checkboxes( + self.faction.air_defense_units, counter, grid, hbox + ) + + self.add_naval_combo = QComboBox() + hbox = self._create_naval_combobox( + self.add_naval_combo, + lambda: self._on_add_unit(self.faction.naval_units, self.add_naval_combo), + ) + grid.addWidget(QLabel("Naval units:"), counter, 0) + counter = self._add_checkboxes(self.faction.naval_units, counter, grid, hbox) + + self.add_missile_combo = QComboBox() + hbox = self._create_unit_combobox( + self.add_missile_combo, + lambda: self._on_add_unit(self.faction.missiles, self.add_missile_combo), + self.faction.missiles, + ["Missile"], + ) + grid.addWidget(QLabel("Missile units:"), counter, 0) + counter = self._add_checkboxes(self.faction.missiles, counter, grid, hbox) if show_jtac: grid.addWidget(QLabel("JTAC"), counter, 0) @@ -166,20 +235,18 @@ class QFactionUnits(QScrollArea): ): for ac_dcs in sorted(AircraftType.each_dcs_type(), key=lambda x: x.id): for ac in AircraftType.for_dcs_type(ac_dcs): - if ac in self.faction.aircraft: + if ( + ac in self.faction.aircraft + or ac in self.faction.awacs + or ac in self.faction.tankers + ): continue predicate(ac) - add_ac = QPushButton("+") - add_ac.setStyleSheet("QPushButton{ font-weight: bold; }") - add_ac.setFixedWidth(50) - add_ac.clicked.connect(callback) - hbox = QHBoxLayout() - hbox.addWidget(cb) - hbox.addWidget(add_ac) + hbox = self._format(cb, callback) return hbox def _create_unit_combobox( - self, cb: QComboBox, callback: Callable, units: Set[GroundUnitType] + self, cb: QComboBox, callback: Callable, units: Set[GroundUnitType], type: list ): for dcs_unit in sorted(GroundUnitType.each_dcs_type(), key=lambda x: x.id): if dcs_unit not in self.faction.country.vehicles: @@ -187,14 +254,32 @@ class QFactionUnits(QScrollArea): for unit in GroundUnitType.for_dcs_type(dcs_unit): if unit in units: continue - cb.addItem(unit.variant_id, unit) - add_unit = QPushButton("+") - add_unit.setStyleSheet("QPushButton{ font-weight: bold; }") - add_unit.setFixedWidth(50) - add_unit.clicked.connect(callback) - hbox = QHBoxLayout() - hbox.addWidget(cb) - hbox.addWidget(add_unit) + if "Frontline vehicles" in type: + cb.addItem(unit.variant_id, unit) + elif unit.unit_class.value in type: + cb.addItem(unit.variant_id, unit) + else: + continue + hbox = self._format(cb, callback) + return hbox + + def _create_naval_combobox(self, cb: QComboBox, callback: Callable): + for ship_dcs in sorted(ShipUnitType.each_dcs_type(), key=lambda x: x.id): + for ship in ShipUnitType.for_dcs_type(ship_dcs): + if ship in self.faction.naval_units: + continue + cb.addItem(ship.variant_id, ship) + hbox = self._format(cb, callback) + return hbox + + def _create_preset_group_combobox(self, cb: QComboBox, callback: Callable): + ForceGroup._load_all() + preset_group_names = {pg.name for pg in self.faction.preset_groups} + for preset_group in ForceGroup._by_name: + if preset_group in preset_group_names: + continue + cb.addItem(preset_group, ForceGroup._by_name[preset_group]) + hbox = self._format(cb, callback) return hbox def _on_add_unit(self, units: Set[UnitType], cb: QComboBox): @@ -211,6 +296,13 @@ class QFactionUnits(QScrollArea): del self.faction.__dict__["all_aircrafts"] self.updateFaction(self.faction) + def _on_add_preset_group(self, groups: List[ForceGroup], cb: QComboBox): + groups.append(cb.currentData()) + if self.faction.__dict__.get("accessible_units"): + # invalidate the cached property + del self.faction.__dict__["accessible_units"] + self.updateFaction(self.faction) + def updateFaction(self, faction: Faction): self.faction = faction self.content = QWidget() @@ -228,6 +320,22 @@ class QFactionUnits(QScrollArea): for d in deletes: units.remove(d) + def _format(self, cb: QComboBox, callback: Callable): + add_button = QPushButton("+") + add_button.setStyleSheet("QPushButton{ font-weight: bold; }") + add_button.setFixedWidth(50) + add_button.clicked.connect(callback) + hbox = QHBoxLayout() + hbox.addWidget(cb) + hbox.addWidget(add_button) + if cb.count() == 0: + cb.setEnabled(False) + add_button.setEnabled(False) + else: + cb.setEnabled(True) + add_button.setEnabled(True) + return hbox + class FactionSelection(QtWidgets.QWizardPage): def __init__(self, parent=None): @@ -284,6 +392,17 @@ class FactionSelection(QtWidgets.QWizardPage): if r == "USA 2005": self.blueFactionSelect.setCurrentIndex(i) + self.saveBlueFactionButton = QPushButton("Save as new faction") + self.saveRedFactionButton = QPushButton("Save as new faction") + self.blueGroupLayout.addWidget(self.saveBlueFactionButton, 3, 0, 1, 2) + self.redGroupLayout.addWidget(self.saveRedFactionButton, 3, 0, 1, 2) + self.saveBlueFactionButton.clicked.connect( + lambda: self.show_save_faction_dialog(self.selected_blue_faction) + ) + self.saveRedFactionButton.clicked.connect( + lambda: self.show_save_faction_dialog(self.selected_red_faction) + ) + # Faction units self.blueFactionUnits = QFactionUnits( self.blueFactionSelect.currentData(), self.blueGroupLayout, show_jtac=True @@ -380,6 +499,15 @@ class FactionSelection(QtWidgets.QWizardPage): qfu.updateFactionUnits(fac.missiles) return fac + def show_save_faction_dialog(self, faction: Faction): + dialog = QFactionSaver(faction) + dialog.exec() + for r in FACTIONS: + if self.blueFactionSelect.findText(r) == -1: + self.blueFactionSelect.addItem(r, FACTIONS[r]) + if self.redFactionSelect.findText(r) == -1: + self.redFactionSelect.addItem(r, FACTIONS[r]) + @property def selected_blue_faction(self) -> Faction: return self._filter_selected_units(self.blueFactionUnits) @@ -387,3 +515,52 @@ class FactionSelection(QtWidgets.QWizardPage): @property def selected_red_faction(self) -> Faction: return self._filter_selected_units(self.redFactionUnits) + + +class QFactionSaver(QDialog): + def __init__(self, faction: Faction): + super().__init__() + self.faction = faction + + self.setMinimumWidth(400) + self.setWindowTitle("Save new faction") + self.setWindowIcon(EVENT_ICONS["strike"]) + + layout = QVBoxLayout() + + self.name_label = QLabel("Name:") + self.name_text = QLineEdit() + layout.addWidget(self.name_label) + layout.addWidget(self.name_text) + + self.description_label = QLabel("Description:") + self.description_text = QLineEdit() + layout.addWidget(self.description_label) + layout.addWidget(self.description_text) + + self.authors_label = QLabel("Author(s):") + self.authors_text = QLineEdit() + layout.addWidget(self.authors_label) + layout.addWidget(self.authors_text) + + self.save_button = QPushButton("Save") + self.save_button.clicked.connect(self.save_faction) + layout.addWidget(self.save_button) + self.setLayout(layout) + + def save_faction(self) -> None: + self.faction.name = self.name_text.text() + self.faction.description = f"
{self.description_text.text()}
" + self.faction.authors = self.authors_text.text() + user_faction_path = persistency.factions_dir() + + fd = QFileDialog( + caption="Save Faction", directory=str(user_faction_path), filter="*.json" + ) + fd.setAcceptMode(QFileDialog.AcceptMode.AcceptSave) + if fd.exec_(): + json_filename = fd.selectedFiles()[0] + with open(json_filename, "w") as file: + json.dump(self.faction.to_dict(), file, indent=2) + FACTIONS.factions[self.faction.name] = self.faction + self.accept()