mirror of
https://github.com/dcs-liberation/dcs_liberation.git
synced 2025-11-10 14:22:26 +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.scenery_group import SceneryGroup
|
||||
from game.utils import Distance, meters, Heading
|
||||
from .controlpoint import (
|
||||
from game.theater.controlpoint import (
|
||||
Airfield,
|
||||
Carrier,
|
||||
ControlPoint,
|
||||
@ -30,7 +30,7 @@ from .controlpoint import (
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .conflicttheater import ConflictTheater
|
||||
from game.theater.conflicttheater import ConflictTheater
|
||||
|
||||
|
||||
class MizCampaignLoader:
|
||||
@ -23,13 +23,11 @@ from .controlpoint import (
|
||||
ControlPoint,
|
||||
MissionTarget,
|
||||
)
|
||||
from .mizcampaignloader import MizCampaignLoader
|
||||
from .seasonalconditions import SeasonalConditions
|
||||
from .frontline import FrontLine
|
||||
from .landmap import Landmap, load_landmap, poly_contains
|
||||
from .latlon import LatLon
|
||||
from .projections import TransverseMercator
|
||||
from ..profiling import logged_duration
|
||||
from .seasonalconditions import SeasonalConditions
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from . import TheaterGroundObject
|
||||
@ -243,30 +241,6 @@ class ConflictTheater:
|
||||
return i
|
||||
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
|
||||
def seasonal_conditions(self) -> SeasonalConditions:
|
||||
raise NotImplementedError
|
||||
|
||||
@ -54,17 +54,6 @@ from ..settings import Settings
|
||||
|
||||
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)
|
||||
class GeneratorSettings:
|
||||
|
||||
@ -27,7 +27,7 @@ from qt_ui import (
|
||||
)
|
||||
from qt_ui.windows.GameUpdateSignal import GameUpdateSignal
|
||||
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.preferences.QLiberationFirstStartWindow import (
|
||||
QLiberationFirstStartWindow,
|
||||
|
||||
@ -1,120 +1,14 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import logging
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, List, Union, Tuple
|
||||
from typing import Optional
|
||||
|
||||
import packaging.version
|
||||
import yaml
|
||||
from PySide2 import QtGui
|
||||
from PySide2.QtCore import QItemSelectionModel, QModelIndex, Qt
|
||||
from PySide2.QtGui import QStandardItem, QStandardItemModel
|
||||
from PySide2.QtWidgets import QAbstractItemView, QListView
|
||||
|
||||
import qt_ui.uiconstants as CONST
|
||||
from game.theater import ConflictTheater
|
||||
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)
|
||||
from game.campaignloader.campaign import Campaign
|
||||
|
||||
|
||||
class QCampaignItem(QStandardItem):
|
||||
@ -144,7 +38,7 @@ class QCampaignList(QListView):
|
||||
self.setup_content(show_incompatible)
|
||||
|
||||
@property
|
||||
def selected_campaign(self) -> Campaign:
|
||||
def selected_campaign(self) -> Optional[Campaign]:
|
||||
return self.currentIndex().data(QCampaignList.CampaignRole)
|
||||
|
||||
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 game import db
|
||||
from game.campaignloader.campaign import Campaign
|
||||
from game.settings import Settings
|
||||
from game.theater.start_generator import GameGenerator, GeneratorSettings, ModSettings
|
||||
from game.factions.faction import Faction
|
||||
from qt_ui.widgets.QLiberationCalendar import QLiberationCalendar
|
||||
from qt_ui.widgets.spinsliders import TenthsSpinSlider, TimeInputs, CurrencySpinner
|
||||
from qt_ui.windows.AirWingConfigurationDialog import AirWingConfigurationDialog
|
||||
from qt_ui.windows.newgame.QCampaignList import (
|
||||
Campaign,
|
||||
QCampaignList,
|
||||
load_campaigns,
|
||||
)
|
||||
from qt_ui.windows.newgame.QCampaignList import QCampaignList
|
||||
|
||||
jinja_env = Environment(
|
||||
loader=FileSystemLoader("resources/ui/templates"),
|
||||
@ -41,7 +38,7 @@ class NewGameWizard(QtWidgets.QWizard):
|
||||
def __init__(self, parent=None):
|
||||
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.addPage(IntroPage())
|
||||
@ -369,6 +366,11 @@ class TheaterConfiguration(QtWidgets.QWizardPage):
|
||||
)
|
||||
campaign = campaignList.selected_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.faction_selection.setDefaultFactions(campaign)
|
||||
self.performanceText.setText(
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user