diff --git a/game/version.py b/game/version.py index 2c886538..67812355 100644 --- a/game/version.py +++ b/game/version.py @@ -53,4 +53,4 @@ VERSION = _build_version_string() #: by a blue circular TriggerZone, campaign creation will fail. Blue circular #: TriggerZones must also have their first property's value field define the type of #: objective (a valid value for a building TGO category, from `game.db.PRICES`). -CAMPAIGN_FORMAT_VERSION = 4 +CAMPAIGN_FORMAT_VERSION = (4, 0) diff --git a/qt_ui/windows/newgame/QCampaignList.py b/qt_ui/windows/newgame/QCampaignList.py index 45ac295b..85d3f0d1 100644 --- a/qt_ui/windows/newgame/QCampaignList.py +++ b/qt_ui/windows/newgame/QCampaignList.py @@ -4,15 +4,16 @@ import json import logging from dataclasses import dataclass from pathlib import Path -from typing import Any, Dict, List, Optional, Union +from typing import Any, Dict, List, Union, Tuple +import packaging.version from PySide2 import QtGui from PySide2.QtCore import QItemSelectionModel from PySide2.QtGui import QStandardItem, QStandardItemModel from PySide2.QtWidgets import QAbstractItemView, QListView import qt_ui.uiconstants as CONST -from game.theater import ConflictTheater, MizCampaignLoader +from game.theater import ConflictTheater from game.version import CAMPAIGN_FORMAT_VERSION PERF_FRIENDLY = 0 @@ -31,7 +32,7 @@ class Campaign: #: 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: int + version: Tuple[int, int] recommended_player_faction: str recommended_enemy_faction: str @@ -45,12 +46,20 @@ class Campaign: 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", ""), - data.get("version", 0), + (version.major, version.minor), data.get("recommended_player_faction", "USA 2005"), data.get("recommended_enemy_faction", "Russia 1990"), data.get("performance", 0), @@ -63,8 +72,15 @@ class Campaign: @property def is_out_of_date(self) -> bool: - """Returns True if this campaign is not up to date with the latest format.""" - return self.version < CAMPAIGN_FORMAT_VERSION + """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: diff --git a/requirements.txt b/requirements.txt index d1698206..7919eb1f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -13,12 +13,14 @@ MarkupSafe==1.1.1 mypy==0.812 mypy-extensions==0.4.3 nodeenv==1.5.0 +packaging==20.9 pathspec==0.8.1 pefile==2019.4.18 Pillow==8.1.1 pre-commit==2.10.1 pyinstaller==4.3 pyinstaller-hooks-contrib==2021.1 +pyparsing==2.4.7 pyproj==3.0.1 PySide2==5.15.2 pywin32-ctypes==0.2.0