mirror of
https://github.com/dcs-retribution/dcs-retribution.git
synced 2025-11-10 15:41:24 +00:00
More campaign loader cleanup.
This commit is contained in:
parent
103675e5bb
commit
d2e22ef8bf
1
game/campaignloader/__init__.py
Normal file
1
game/campaignloader/__init__.py
Normal file
@ -0,0 +1 @@
|
|||||||
|
from .campaign import Campaign
|
||||||
148
game/campaignloader/campaign.py
Normal file
148
game/campaignloader/campaign.py
Normal file
@ -0,0 +1,148 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
from collections import Iterator
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Tuple, Dict, Any
|
||||||
|
|
||||||
|
from packaging.version import Version
|
||||||
|
import yaml
|
||||||
|
|
||||||
|
from game.campaignloader.mizcampaignloader import MizCampaignLoader
|
||||||
|
from game.profiling import logged_duration
|
||||||
|
from game.theater import (
|
||||||
|
ConflictTheater,
|
||||||
|
CaucasusTheater,
|
||||||
|
NevadaTheater,
|
||||||
|
PersianGulfTheater,
|
||||||
|
NormandyTheater,
|
||||||
|
TheChannelTheater,
|
||||||
|
SyriaTheater,
|
||||||
|
MarianaIslandsTheater,
|
||||||
|
)
|
||||||
|
from game.version import CAMPAIGN_FORMAT_VERSION
|
||||||
|
|
||||||
|
|
||||||
|
PERF_FRIENDLY = 0
|
||||||
|
PERF_MEDIUM = 1
|
||||||
|
PERF_HARD = 2
|
||||||
|
PERF_NASA = 3
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class Campaign:
|
||||||
|
name: str
|
||||||
|
icon_name: str
|
||||||
|
authors: str
|
||||||
|
description: str
|
||||||
|
|
||||||
|
#: The revision of the campaign format the campaign was built for. We do not attempt
|
||||||
|
#: to migrate old campaigns, but this is used to show a warning in the UI when
|
||||||
|
#: selecting a campaign that is not up to date.
|
||||||
|
version: Tuple[int, int]
|
||||||
|
|
||||||
|
recommended_player_faction: str
|
||||||
|
recommended_enemy_faction: str
|
||||||
|
performance: int
|
||||||
|
data: Dict[str, Any]
|
||||||
|
path: Path
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_file(cls, path: Path) -> Campaign:
|
||||||
|
with path.open() as campaign_file:
|
||||||
|
if path.suffix == ".yaml":
|
||||||
|
data = yaml.safe_load(campaign_file)
|
||||||
|
else:
|
||||||
|
data = json.load(campaign_file)
|
||||||
|
|
||||||
|
sanitized_theater = data["theater"].replace(" ", "")
|
||||||
|
version_field = data.get("version", "0")
|
||||||
|
try:
|
||||||
|
version = Version(version_field)
|
||||||
|
except TypeError:
|
||||||
|
logging.warning(
|
||||||
|
f"Non-string campaign version in {path}. Parse may be incorrect."
|
||||||
|
)
|
||||||
|
version = Version(str(version_field))
|
||||||
|
return cls(
|
||||||
|
data["name"],
|
||||||
|
f"Terrain_{sanitized_theater}",
|
||||||
|
data.get("authors", "???"),
|
||||||
|
data.get("description", ""),
|
||||||
|
(version.major, version.minor),
|
||||||
|
data.get("recommended_player_faction", "USA 2005"),
|
||||||
|
data.get("recommended_enemy_faction", "Russia 1990"),
|
||||||
|
data.get("performance", 0),
|
||||||
|
data,
|
||||||
|
path,
|
||||||
|
)
|
||||||
|
|
||||||
|
def load_theater(self) -> ConflictTheater:
|
||||||
|
theaters = {
|
||||||
|
"Caucasus": CaucasusTheater,
|
||||||
|
"Nevada": NevadaTheater,
|
||||||
|
"Persian Gulf": PersianGulfTheater,
|
||||||
|
"Normandy": NormandyTheater,
|
||||||
|
"The Channel": TheChannelTheater,
|
||||||
|
"Syria": SyriaTheater,
|
||||||
|
"MarianaIslands": MarianaIslandsTheater,
|
||||||
|
}
|
||||||
|
theater = theaters[self.data["theater"]]
|
||||||
|
t = theater()
|
||||||
|
|
||||||
|
try:
|
||||||
|
miz = self.data["miz"]
|
||||||
|
except KeyError as ex:
|
||||||
|
raise RuntimeError(
|
||||||
|
"Old format (non-miz) campaigns are no longer supported."
|
||||||
|
) from ex
|
||||||
|
|
||||||
|
with logged_duration("Importing miz data"):
|
||||||
|
MizCampaignLoader(self.path.parent / miz, t).populate_theater()
|
||||||
|
return t
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_out_of_date(self) -> bool:
|
||||||
|
"""Returns True if this campaign is not up to date with the latest format.
|
||||||
|
|
||||||
|
This is more permissive than is_from_future, which is sensitive to minor version
|
||||||
|
bumps (the old game definitely doesn't support the minor features added in the
|
||||||
|
new version, and the campaign may require them. However, the minor version only
|
||||||
|
indicates *optional* new features, so we do not need to mark out of date
|
||||||
|
campaigns as incompatible if they are within the same major version.
|
||||||
|
"""
|
||||||
|
return self.version[0] < CAMPAIGN_FORMAT_VERSION[0]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_from_future(self) -> bool:
|
||||||
|
"""Returns True if this campaign is newer than the supported format."""
|
||||||
|
return self.version > CAMPAIGN_FORMAT_VERSION
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_compatible(self) -> bool:
|
||||||
|
"""Returns True is this campaign was built for this version of the game."""
|
||||||
|
if self.version == (0, 0):
|
||||||
|
return False
|
||||||
|
if self.is_out_of_date:
|
||||||
|
return False
|
||||||
|
if self.is_from_future:
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def iter_campaign_defs() -> Iterator[Path]:
|
||||||
|
campaign_dir = Path("resources/campaigns")
|
||||||
|
yield from campaign_dir.glob("*.json")
|
||||||
|
yield from campaign_dir.glob("*.yaml")
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def load_each(cls) -> Iterator[Campaign]:
|
||||||
|
for path in cls.iter_campaign_defs():
|
||||||
|
try:
|
||||||
|
logging.debug(f"Loading campaign from {path}...")
|
||||||
|
campaign = Campaign.from_file(path)
|
||||||
|
yield campaign
|
||||||
|
except RuntimeError:
|
||||||
|
logging.exception(f"Unable to load campaign from {path}")
|
||||||
@ -20,7 +20,7 @@ from game.positioned import Positioned
|
|||||||
from game.profiling import logged_duration
|
from game.profiling import logged_duration
|
||||||
from game.scenery_group import SceneryGroup
|
from game.scenery_group import SceneryGroup
|
||||||
from game.utils import Distance, meters, Heading
|
from game.utils import Distance, meters, Heading
|
||||||
from .controlpoint import (
|
from game.theater.controlpoint import (
|
||||||
Airfield,
|
Airfield,
|
||||||
Carrier,
|
Carrier,
|
||||||
ControlPoint,
|
ControlPoint,
|
||||||
@ -30,7 +30,7 @@ from .controlpoint import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from .conflicttheater import ConflictTheater
|
from game.theater.conflicttheater import ConflictTheater
|
||||||
|
|
||||||
|
|
||||||
class MizCampaignLoader:
|
class MizCampaignLoader:
|
||||||
@ -23,13 +23,11 @@ from .controlpoint import (
|
|||||||
ControlPoint,
|
ControlPoint,
|
||||||
MissionTarget,
|
MissionTarget,
|
||||||
)
|
)
|
||||||
from .mizcampaignloader import MizCampaignLoader
|
|
||||||
from .seasonalconditions import SeasonalConditions
|
|
||||||
from .frontline import FrontLine
|
from .frontline import FrontLine
|
||||||
from .landmap import Landmap, load_landmap, poly_contains
|
from .landmap import Landmap, load_landmap, poly_contains
|
||||||
from .latlon import LatLon
|
from .latlon import LatLon
|
||||||
from .projections import TransverseMercator
|
from .projections import TransverseMercator
|
||||||
from ..profiling import logged_duration
|
from .seasonalconditions import SeasonalConditions
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from . import TheaterGroundObject
|
from . import TheaterGroundObject
|
||||||
@ -243,30 +241,6 @@ class ConflictTheater:
|
|||||||
return i
|
return i
|
||||||
raise KeyError(f"Cannot find ControlPoint with ID {id}")
|
raise KeyError(f"Cannot find ControlPoint with ID {id}")
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def from_file_data(directory: Path, data: Dict[str, Any]) -> ConflictTheater:
|
|
||||||
theaters = {
|
|
||||||
"Caucasus": CaucasusTheater,
|
|
||||||
"Nevada": NevadaTheater,
|
|
||||||
"Persian Gulf": PersianGulfTheater,
|
|
||||||
"Normandy": NormandyTheater,
|
|
||||||
"The Channel": TheChannelTheater,
|
|
||||||
"Syria": SyriaTheater,
|
|
||||||
"MarianaIslands": MarianaIslandsTheater,
|
|
||||||
}
|
|
||||||
theater = theaters[data["theater"]]
|
|
||||||
t = theater()
|
|
||||||
|
|
||||||
miz = data.get("miz", None)
|
|
||||||
if miz is None:
|
|
||||||
raise RuntimeError(
|
|
||||||
"Old format (non-miz) campaigns are no longer supported."
|
|
||||||
)
|
|
||||||
|
|
||||||
with logged_duration("Importing miz data"):
|
|
||||||
MizCampaignLoader(directory / miz, t).populate_theater()
|
|
||||||
return t
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def seasonal_conditions(self) -> SeasonalConditions:
|
def seasonal_conditions(self) -> SeasonalConditions:
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|||||||
@ -54,17 +54,6 @@ from ..settings import Settings
|
|||||||
|
|
||||||
GroundObjectTemplates = Dict[str, Dict[str, Any]]
|
GroundObjectTemplates = Dict[str, Dict[str, Any]]
|
||||||
|
|
||||||
UNIT_VARIETY = 6
|
|
||||||
UNIT_AMOUNT_FACTOR = 16
|
|
||||||
UNIT_COUNT_IMPORTANCE_LOG = 1.3
|
|
||||||
|
|
||||||
COUNT_BY_TASK = {
|
|
||||||
PinpointStrike: 12,
|
|
||||||
CAP: 8,
|
|
||||||
CAS: 4,
|
|
||||||
AirDefence: 1,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
class GeneratorSettings:
|
class GeneratorSettings:
|
||||||
|
|||||||
@ -27,7 +27,7 @@ from qt_ui import (
|
|||||||
)
|
)
|
||||||
from qt_ui.windows.GameUpdateSignal import GameUpdateSignal
|
from qt_ui.windows.GameUpdateSignal import GameUpdateSignal
|
||||||
from qt_ui.windows.QLiberationWindow import QLiberationWindow
|
from qt_ui.windows.QLiberationWindow import QLiberationWindow
|
||||||
from qt_ui.windows.newgame.QCampaignList import Campaign
|
from game.campaignloader.campaign import Campaign
|
||||||
from qt_ui.windows.newgame.QNewGameWizard import DEFAULT_BUDGET
|
from qt_ui.windows.newgame.QNewGameWizard import DEFAULT_BUDGET
|
||||||
from qt_ui.windows.preferences.QLiberationFirstStartWindow import (
|
from qt_ui.windows.preferences.QLiberationFirstStartWindow import (
|
||||||
QLiberationFirstStartWindow,
|
QLiberationFirstStartWindow,
|
||||||
|
|||||||
@ -1,120 +1,14 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import json
|
from typing import Optional
|
||||||
import logging
|
|
||||||
from dataclasses import dataclass
|
|
||||||
from pathlib import Path
|
|
||||||
from typing import Any, Dict, List, Union, Tuple
|
|
||||||
|
|
||||||
import packaging.version
|
|
||||||
import yaml
|
|
||||||
from PySide2 import QtGui
|
from PySide2 import QtGui
|
||||||
from PySide2.QtCore import QItemSelectionModel, QModelIndex, Qt
|
from PySide2.QtCore import QItemSelectionModel, QModelIndex, Qt
|
||||||
from PySide2.QtGui import QStandardItem, QStandardItemModel
|
from PySide2.QtGui import QStandardItem, QStandardItemModel
|
||||||
from PySide2.QtWidgets import QAbstractItemView, QListView
|
from PySide2.QtWidgets import QAbstractItemView, QListView
|
||||||
|
|
||||||
import qt_ui.uiconstants as CONST
|
import qt_ui.uiconstants as CONST
|
||||||
from game.theater import ConflictTheater
|
from game.campaignloader.campaign import Campaign
|
||||||
from game.version import CAMPAIGN_FORMAT_VERSION
|
|
||||||
|
|
||||||
PERF_FRIENDLY = 0
|
|
||||||
PERF_MEDIUM = 1
|
|
||||||
PERF_HARD = 2
|
|
||||||
PERF_NASA = 3
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
|
||||||
class Campaign:
|
|
||||||
name: str
|
|
||||||
icon_name: str
|
|
||||||
authors: str
|
|
||||||
description: str
|
|
||||||
|
|
||||||
#: The revision of the campaign format the campaign was built for. We do not attempt
|
|
||||||
#: to migrate old campaigns, but this is used to show a warning in the UI when
|
|
||||||
#: selecting a campaign that is not up to date.
|
|
||||||
version: Tuple[int, int]
|
|
||||||
|
|
||||||
recommended_player_faction: str
|
|
||||||
recommended_enemy_faction: str
|
|
||||||
performance: Union[PERF_FRIENDLY, PERF_MEDIUM, PERF_HARD, PERF_NASA]
|
|
||||||
data: Dict[str, Any]
|
|
||||||
path: Path
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def from_file(cls, path: Path) -> Campaign:
|
|
||||||
with path.open(encoding="utf-8") as campaign_file:
|
|
||||||
if path.suffix == ".yaml":
|
|
||||||
data = yaml.safe_load(campaign_file)
|
|
||||||
else:
|
|
||||||
data = json.load(campaign_file)
|
|
||||||
|
|
||||||
sanitized_theater = data["theater"].replace(" ", "")
|
|
||||||
version_field = data.get("version", "0")
|
|
||||||
try:
|
|
||||||
version = packaging.version.parse(version_field)
|
|
||||||
except TypeError:
|
|
||||||
logging.warning(
|
|
||||||
f"Non-string campaign version in {path}. Parse may be incorrect."
|
|
||||||
)
|
|
||||||
version = packaging.version.parse(str(version_field))
|
|
||||||
return cls(
|
|
||||||
data["name"],
|
|
||||||
f"Terrain_{sanitized_theater}",
|
|
||||||
data.get("authors", "???"),
|
|
||||||
data.get("description", ""),
|
|
||||||
(version.major, version.minor),
|
|
||||||
data.get("recommended_player_faction", "USA 2005"),
|
|
||||||
data.get("recommended_enemy_faction", "Russia 1990"),
|
|
||||||
data.get("performance", 0),
|
|
||||||
data,
|
|
||||||
path,
|
|
||||||
)
|
|
||||||
|
|
||||||
def load_theater(self) -> ConflictTheater:
|
|
||||||
return ConflictTheater.from_file_data(self.path.parent, self.data)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def is_out_of_date(self) -> bool:
|
|
||||||
"""Returns True if this campaign is not up to date with the latest format.
|
|
||||||
|
|
||||||
This is more permissive than is_from_future, which is sensitive to minor version
|
|
||||||
bumps (the old game definitely doesn't support the minor features added in the
|
|
||||||
new version, and the campaign may require them. However, the minor version only
|
|
||||||
indicates *optional* new features, so we do not need to mark out of date
|
|
||||||
campaigns as incompatible if they are within the same major version.
|
|
||||||
"""
|
|
||||||
return self.version[0] < CAMPAIGN_FORMAT_VERSION[0]
|
|
||||||
|
|
||||||
@property
|
|
||||||
def is_from_future(self) -> bool:
|
|
||||||
"""Returns True if this campaign is newer than the supported format."""
|
|
||||||
return self.version > CAMPAIGN_FORMAT_VERSION
|
|
||||||
|
|
||||||
@property
|
|
||||||
def is_compatible(self) -> bool:
|
|
||||||
"""Returns True is this campaign was built for this version of the game."""
|
|
||||||
if not self.version:
|
|
||||||
return False
|
|
||||||
if self.is_out_of_date:
|
|
||||||
return False
|
|
||||||
if self.is_from_future:
|
|
||||||
return False
|
|
||||||
return True
|
|
||||||
|
|
||||||
|
|
||||||
def load_campaigns() -> List[Campaign]:
|
|
||||||
campaign_dir = Path("resources/campaigns")
|
|
||||||
campaigns = []
|
|
||||||
for path in campaign_dir.glob("*.json"):
|
|
||||||
try:
|
|
||||||
logging.debug(f"Loading campaign from {path}...")
|
|
||||||
campaign = Campaign.from_file(path)
|
|
||||||
campaigns.append(campaign)
|
|
||||||
except RuntimeError:
|
|
||||||
logging.exception(f"Unable to load campaign from {path}")
|
|
||||||
|
|
||||||
return sorted(campaigns, key=lambda x: x.name)
|
|
||||||
|
|
||||||
|
|
||||||
class QCampaignItem(QStandardItem):
|
class QCampaignItem(QStandardItem):
|
||||||
@ -144,7 +38,7 @@ class QCampaignList(QListView):
|
|||||||
self.setup_content(show_incompatible)
|
self.setup_content(show_incompatible)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def selected_campaign(self) -> Campaign:
|
def selected_campaign(self) -> Optional[Campaign]:
|
||||||
return self.currentIndex().data(QCampaignList.CampaignRole)
|
return self.currentIndex().data(QCampaignList.CampaignRole)
|
||||||
|
|
||||||
def setup_content(self, show_incompatible: bool) -> None:
|
def setup_content(self, show_incompatible: bool) -> None:
|
||||||
|
|||||||
@ -10,17 +10,14 @@ from PySide2.QtWidgets import QVBoxLayout, QTextEdit, QLabel, QCheckBox
|
|||||||
from jinja2 import Environment, FileSystemLoader, select_autoescape
|
from jinja2 import Environment, FileSystemLoader, select_autoescape
|
||||||
|
|
||||||
from game import db
|
from game import db
|
||||||
|
from game.campaignloader.campaign import Campaign
|
||||||
from game.settings import Settings
|
from game.settings import Settings
|
||||||
from game.theater.start_generator import GameGenerator, GeneratorSettings, ModSettings
|
from game.theater.start_generator import GameGenerator, GeneratorSettings, ModSettings
|
||||||
from game.factions.faction import Faction
|
from game.factions.faction import Faction
|
||||||
from qt_ui.widgets.QLiberationCalendar import QLiberationCalendar
|
from qt_ui.widgets.QLiberationCalendar import QLiberationCalendar
|
||||||
from qt_ui.widgets.spinsliders import TenthsSpinSlider, TimeInputs, CurrencySpinner
|
from qt_ui.widgets.spinsliders import TenthsSpinSlider, TimeInputs, CurrencySpinner
|
||||||
from qt_ui.windows.AirWingConfigurationDialog import AirWingConfigurationDialog
|
from qt_ui.windows.AirWingConfigurationDialog import AirWingConfigurationDialog
|
||||||
from qt_ui.windows.newgame.QCampaignList import (
|
from qt_ui.windows.newgame.QCampaignList import QCampaignList
|
||||||
Campaign,
|
|
||||||
QCampaignList,
|
|
||||||
load_campaigns,
|
|
||||||
)
|
|
||||||
|
|
||||||
jinja_env = Environment(
|
jinja_env = Environment(
|
||||||
loader=FileSystemLoader("resources/ui/templates"),
|
loader=FileSystemLoader("resources/ui/templates"),
|
||||||
@ -41,7 +38,7 @@ class NewGameWizard(QtWidgets.QWizard):
|
|||||||
def __init__(self, parent=None):
|
def __init__(self, parent=None):
|
||||||
super(NewGameWizard, self).__init__(parent)
|
super(NewGameWizard, self).__init__(parent)
|
||||||
|
|
||||||
self.campaigns = load_campaigns()
|
self.campaigns = list(sorted(Campaign.load_each(), key=lambda x: x.name))
|
||||||
|
|
||||||
self.faction_selection_page = FactionSelection()
|
self.faction_selection_page = FactionSelection()
|
||||||
self.addPage(IntroPage())
|
self.addPage(IntroPage())
|
||||||
@ -369,6 +366,11 @@ class TheaterConfiguration(QtWidgets.QWizardPage):
|
|||||||
)
|
)
|
||||||
campaign = campaignList.selected_campaign
|
campaign = campaignList.selected_campaign
|
||||||
self.setField("selectedCampaign", campaign)
|
self.setField("selectedCampaign", campaign)
|
||||||
|
if campaign is None:
|
||||||
|
self.campaignMapDescription.setText("No campaign selected")
|
||||||
|
self.performanceText.setText("No campaign selected")
|
||||||
|
return
|
||||||
|
|
||||||
self.campaignMapDescription.setText(template.render({"campaign": campaign}))
|
self.campaignMapDescription.setText(template.render({"campaign": campaign}))
|
||||||
self.faction_selection.setDefaultFactions(campaign)
|
self.faction_selection.setDefaultFactions(campaign)
|
||||||
self.performanceText.setText(
|
self.performanceText.setText(
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user