diff --git a/cfg/converter/games/game_editions.toml b/cfg/converter/games/game_editions.toml index 7b0331e81e..24628e8273 100644 --- a/cfg/converter/games/game_editions.toml +++ b/cfg/converter/games/game_editions.toml @@ -37,6 +37,11 @@ expansions = [] "C:/Program Files (x86)/Microsoft Games/Age of Empires II", ] + [AOC.targetmods.aoe2_base] + version = "0.5.1" + versionstr = "1.0c" + min_api_version = "0.5.0" + [AOCDEMO] name = "Age of Empires 2: The Conqueror's Trial Version" @@ -57,6 +62,12 @@ expansions = [] terrain = ["data/Terrain.drs"] blend = ["data/blendomatic.dat"] + [AOCDEMO.targetmods.trial_base] + version = "0.5.1" + versionstr = "Trial" + min_api_version = "0.5.0" + + [AOK] name = "Age of Empires 2: Age of Kings" game_edition_id = "AOK" @@ -90,6 +101,8 @@ expansions = [] "C:/Program Files (x86)/Microsoft Games/Age of Empires II", ] + [AOK.targetmods] + [AOE1DE] name = "Age of Empires 1: Definitive Edition (Steam)" @@ -131,6 +144,11 @@ expansions = [] "C:/Program Files (x86)/Steam/steamapps/common/AoEDE", ] + [AOE1DE.targetmods.de1_base] + version = "0.5.1" + versionstr = "1.0a" + min_api_version = "0.5.0" + [ROR] name = "Age of Empires 1: Rise of Rome" @@ -166,6 +184,11 @@ expansions = [] "C:/Program Files (x86)/Microsoft Games/Age of Empires", ] + [ROR.targetmods.aoe1_base] + version = "0.5.1" + versionstr = "1.0a" + min_api_version = "0.5.0" + [HDEDITION] name = "Age of Empires 2: HD Edition" @@ -205,6 +228,11 @@ expansions = [] "C:/Program Files (x86)/Steam/steamapps/common/Age2HD", ] + [HDEDITION.targetmods.hd_base] + version = "0.5.1" + versionstr = "5.8" + min_api_version = "0.5.0" + [AOE2DE] name = "Age of Empires 2: Definitive Edition" @@ -249,6 +277,11 @@ expansions = [] "C:/Program Files (x86)/Steam/steamapps/common/AoE2DE", ] + [AOE2DE.targetmods.de2_base] + version = "0.6.0" + versionstr = "Update 118476+" + min_api_version = "0.5.0" + [SWGB] name = "Star Wars: Galactic Battlegrounds" @@ -285,3 +318,8 @@ expansions = ["SWGB_CC"] "C:/Program Files (x86)/Steam/steamapps/common/STAR WARS - Galactic Battlegrounds Saga", "C:/Program Files/Steam/steamapps/common/STAR WARS - Galactic Battlegrounds Saga", ] + + [SWGB.targetmods.swgb_base] + version = "0.5.1" + versionstr = "1.1-gog4" + min_api_version = "0.5.0" diff --git a/cfg/converter/games/game_expansions.toml b/cfg/converter/games/game_expansions.toml index b3dd159c12..5aca31f780 100644 --- a/cfg/converter/games/game_expansions.toml +++ b/cfg/converter/games/game_expansions.toml @@ -3,31 +3,36 @@ file_version = "1.0" [AFRI_KING] -name = "African Kingdoms (HD)" +name = "African Kingdoms (HD)" game_edition_id = "AFRI_KING" -subfolder = "hd_ak" -support = "nope" -targetmod = [ "aoe2-ak", "aoe2-ak-graphics" ] +subfolder = "hd_ak" +support = "nope" +targetmod = ["aoe2-ak", "aoe2-ak-graphics"] [AFRI_KING.mediapaths] - graphics = [ "resources/_common/slp/" ] - sounds = [ "resources/_common/sound/" ] - interface = [ "resources/_common/drs/interface/" ] - terrain = [ "resources/_common/terrain/" ] + graphics = ["resources/_common/slp/"] + sounds = ["resources/_common/sound/"] + interface = ["resources/_common/drs/interface/"] + terrain = ["resources/_common/terrain/"] + + [AFRI_KING.targetmods] + [SWGB_CC] -name = "Clone Campaigns" +name = "Clone Campaigns" game_edition_id = "SWGB_CC" -subfolder = "swgb_cc" -support = "yes" -targetmod = [ "swgb-cc", "swgb-cc-graphics" ] +subfolder = "swgb_cc" +support = "yes" +targetmod = ["swgb-cc", "swgb-cc-graphics"] [SWGB_CC.mediapaths] - datfile = [ "Game/Data/genie_x1.dat" ] - gamedata = [ "Game/Data/genie_x1.dat" ] - graphics = [ "Game/Data/graphics_x1.drs" ] - language = [ "Game/language_x1.dll" ] - palettes = [ "Game/Data/interfac_x1.drs" ] - sounds = [ "Game/Data/sounds_x1.drs" ] - interface = [ "Game/Data/interfac_x1.drs" ] - terrain = [ "Game/Data/terrain_x1.drs" ] + datfile = ["Game/Data/genie_x1.dat"] + gamedata = ["Game/Data/genie_x1.dat"] + graphics = ["Game/Data/graphics_x1.drs"] + language = ["Game/language_x1.dll"] + palettes = ["Game/Data/interfac_x1.drs"] + sounds = ["Game/Data/sounds_x1.drs"] + interface = ["Game/Data/interfac_x1.drs"] + terrain = ["Game/Data/terrain_x1.drs"] + + [SWGB_CC.targetmods] diff --git a/openage/convert/main.py b/openage/convert/main.py index f89356402f..cefa06d44e 100644 --- a/openage/convert/main.py +++ b/openage/convert/main.py @@ -15,11 +15,13 @@ from ..util.fslike.wrapper import (DirectoryCreator, Synchronizer as AccessSynchronizer) from .service.debug_info import debug_cli_args, debug_game_version, debug_mounts -from .service.init.conversion_required import conversion_required +from .service.init.changelog import check_updates +from .service.init.modpack_search import enumerate_modpacks from .service.init.mount_asset_dirs import mount_asset_dirs from .service.init.version_detect import create_version_objects from .tool.interactive import interactive_browser -from .tool.subtool.acquire_sourcedir import acquire_conversion_source_dir, wanna_convert +from .tool.subtool.acquire_sourcedir import acquire_conversion_source_dir, wanna_convert, \ + wanna_check_updates from .tool.subtool.version_select import get_game_version if typing.TYPE_CHECKING: @@ -238,6 +240,16 @@ def init_subparser(cli: ArgumentParser): "--export-api", action='store_true', help="Export the openage nyan API definition as a modpack") + cli.add_argument( + "--check-updates", action='store_true', + help="Check if the assets are up to date" + ) + + cli.add_argument( + "--no-prompts", action='store_false', dest='show_prompts', + help="Disable user prompts" + ) + def main(args, error): """ CLI entry point """ @@ -268,11 +280,16 @@ def main(args, error): from ..assets import get_asset_path outdir = get_asset_path(args.output_dir) - if args.force or wanna_convert() or conversion_required(outdir): + if args.force or (args.show_prompts and wanna_convert()): convert_assets(outdir, args, srcdir) - else: - print("assets are up to date; no conversion is required.") - print("override with --force.") + if args.check_updates or (args.show_prompts and wanna_check_updates()): + # check if the assets are up to date + modpack_dir = outdir / "converted" + available_modpacks = enumerate_modpacks(modpack_dir, exclude={"engine"}) + + game_info_dir = args.cfg_dir / "converter" / "games" + + check_updates(available_modpacks, game_info_dir) return 0 diff --git a/openage/convert/processor/conversion/aoc/modpack_subprocessor.py b/openage/convert/processor/conversion/aoc/modpack_subprocessor.py index 4f7628751e..b842fc74c2 100644 --- a/openage/convert/processor/conversion/aoc/modpack_subprocessor.py +++ b/openage/convert/processor/conversion/aoc/modpack_subprocessor.py @@ -44,7 +44,10 @@ def _get_aoe2_base(cls, full_data_set: GenieObjectContainer) -> Modpack: mod_def = modpack.get_info() - mod_def.set_info("aoe2_base", "0.5.1", versionstr="1.0c", repo="openage") + targetmod_info = full_data_set.game_version.edition.target_modpacks["aoe2_base"] + version = targetmod_info["version"] + versionstr = targetmod_info["versionstr"] + mod_def.set_info("aoe2_base", version, versionstr=versionstr, repo="openage") mod_def.add_include("data/**") diff --git a/openage/convert/processor/conversion/aoc_demo/modpack_subprocessor.py b/openage/convert/processor/conversion/aoc_demo/modpack_subprocessor.py index 8ae5c83705..0759ed9127 100644 --- a/openage/convert/processor/conversion/aoc_demo/modpack_subprocessor.py +++ b/openage/convert/processor/conversion/aoc_demo/modpack_subprocessor.py @@ -1,4 +1,4 @@ -# Copyright 2023-2023 the openage authors. See copying.md for legal info. +# Copyright 2023-2024 the openage authors. See copying.md for legal info. # # pylint: disable=too-few-public-methods @@ -40,7 +40,10 @@ def _get_demo_base(cls, full_data_set: GenieObjectContainer) -> Modpack: mod_def = modpack.get_info() - mod_def.set_info("trial_base", "0.5.1", versionstr="Trial", repo="openage") + targetmod_info = full_data_set.game_version.edition.target_modpacks["trial_base"] + version = targetmod_info["version"] + versionstr = targetmod_info["versionstr"] + mod_def.set_info("trial_base", version, versionstr=versionstr, repo="openage") mod_def.add_include("data/**") diff --git a/openage/convert/processor/conversion/de1/modpack_subprocessor.py b/openage/convert/processor/conversion/de1/modpack_subprocessor.py index 1a431e81d2..55c143ee83 100644 --- a/openage/convert/processor/conversion/de1/modpack_subprocessor.py +++ b/openage/convert/processor/conversion/de1/modpack_subprocessor.py @@ -1,4 +1,4 @@ -# Copyright 2023-2023 the openage authors. See copying.md for legal info. +# Copyright 2023-2024 the openage authors. See copying.md for legal info. # # pylint: disable=too-few-public-methods @@ -40,7 +40,10 @@ def _get_aoe1_base(cls, full_data_set: GenieObjectContainer) -> Modpack: mod_def = modpack.get_info() - mod_def.set_info("de1_base", "0.5.1", versionstr="1.0a", repo="openage") + targetmod_info = full_data_set.game_version.edition.target_modpacks["de1_base"] + version = targetmod_info["version"] + versionstr = targetmod_info["versionstr"] + mod_def.set_info("de1_base", version, versionstr=versionstr, repo="openage") mod_def.add_include("data/**") diff --git a/openage/convert/processor/conversion/de2/modpack_subprocessor.py b/openage/convert/processor/conversion/de2/modpack_subprocessor.py index 5af6fdf8b5..ab09c233a6 100644 --- a/openage/convert/processor/conversion/de2/modpack_subprocessor.py +++ b/openage/convert/processor/conversion/de2/modpack_subprocessor.py @@ -40,7 +40,10 @@ def _get_aoe2_base(cls, full_data_set: GenieObjectContainer) -> Modpack: mod_def = modpack.get_info() - mod_def.set_info("de2_base", "0.6.0", versionstr="Update 118476+", repo="openage") + targetmod_info = full_data_set.game_version.edition.target_modpacks["de2_base"] + version = targetmod_info["version"] + versionstr = targetmod_info["versionstr"] + mod_def.set_info("de2_base", version, versionstr=versionstr, repo="openage") mod_def.add_include("data/**") diff --git a/openage/convert/processor/conversion/hd/modpack_subprocessor.py b/openage/convert/processor/conversion/hd/modpack_subprocessor.py index 8be52ac744..536c451ae1 100644 --- a/openage/convert/processor/conversion/hd/modpack_subprocessor.py +++ b/openage/convert/processor/conversion/hd/modpack_subprocessor.py @@ -1,4 +1,4 @@ -# Copyright 2021-2023 the openage authors. See copying.md for legal info. +# Copyright 2021-2024 the openage authors. See copying.md for legal info. # # pylint: disable=too-few-public-methods @@ -40,7 +40,10 @@ def _get_aoe2_base(cls, full_data_set: GenieObjectContainer) -> Modpack: mod_def = modpack.get_info() - mod_def.set_info("hd_base", "0.5.1", versionstr="5.8", repo="openage") + targetmod_info = full_data_set.game_version.edition.target_modpacks["hd_base"] + version = targetmod_info["version"] + versionstr = targetmod_info["versionstr"] + mod_def.set_info("hd_base", version, versionstr=versionstr, repo="openage") mod_def.add_include("data/**") diff --git a/openage/convert/processor/conversion/ror/modpack_subprocessor.py b/openage/convert/processor/conversion/ror/modpack_subprocessor.py index ff7746e861..b0022c601e 100644 --- a/openage/convert/processor/conversion/ror/modpack_subprocessor.py +++ b/openage/convert/processor/conversion/ror/modpack_subprocessor.py @@ -1,4 +1,4 @@ -# Copyright 2020-2023 the openage authors. See copying.md for legal info. +# Copyright 2020-2024 the openage authors. See copying.md for legal info. # # pylint: disable=too-few-public-methods @@ -41,7 +41,10 @@ def _get_aoe1_base(cls, full_data_set: GenieObjectContainer) -> Modpack: mod_def = modpack.get_info() - mod_def.set_info("aoe1_base", "0.5.1", versionstr="1.0a", repo="openage") + targetmod_info = full_data_set.game_version.edition.target_modpacks["aoe1_base"] + version = targetmod_info["version"] + versionstr = targetmod_info["versionstr"] + mod_def.set_info("aoe1_base", version, versionstr=versionstr, repo="openage") mod_def.add_include("data/**") diff --git a/openage/convert/processor/conversion/swgbcc/modpack_subprocessor.py b/openage/convert/processor/conversion/swgbcc/modpack_subprocessor.py index 5a69a47516..262926b919 100644 --- a/openage/convert/processor/conversion/swgbcc/modpack_subprocessor.py +++ b/openage/convert/processor/conversion/swgbcc/modpack_subprocessor.py @@ -1,4 +1,4 @@ -# Copyright 2020-2023 the openage authors. See copying.md for legal info. +# Copyright 2020-2024 the openage authors. See copying.md for legal info. # # pylint: disable=too-few-public-methods @@ -40,7 +40,10 @@ def _get_swgb_base(cls, full_data_set: GenieObjectContainer) -> Modpack: mod_def = modpack.get_info() - mod_def.set_info("swgb_base", "0.5.1", versionstr="1.1-gog4", repo="openage") + targetmod_info = full_data_set.game_version.edition.target_modpacks["swgb_base"] + version = targetmod_info["version"] + versionstr = targetmod_info["versionstr"] + mod_def.set_info("swgb_base", version, versionstr=versionstr, repo="openage") mod_def.add_include("data/**") diff --git a/openage/convert/service/init/CMakeLists.txt b/openage/convert/service/init/CMakeLists.txt index aa588dcb29..1d09b906d9 100644 --- a/openage/convert/service/init/CMakeLists.txt +++ b/openage/convert/service/init/CMakeLists.txt @@ -1,7 +1,7 @@ add_py_modules( __init__.py api_export_required.py - conversion_required.py + changelog.py modpack_search.py mount_asset_dirs.py version_detect.py diff --git a/openage/convert/service/init/changelog.py b/openage/convert/service/init/changelog.py new file mode 100644 index 0000000000..20877664e4 --- /dev/null +++ b/openage/convert/service/init/changelog.py @@ -0,0 +1,49 @@ +# Copyright 2024-2024 the openage authors. See copying.md for legal info. + +""" +Check for updates in the openage converter modpacks. +""" +from __future__ import annotations +import typing + +from itertools import chain + +from openage.util.version import SemanticVersion + +from ....log import info + +from ..init.version_detect import create_version_objects + + +if typing.TYPE_CHECKING: + from openage.util.fslike.union import UnionPath + + +def check_updates(available_modpacks: dict[str, str], game_info_dir: UnionPath): + """ + Check if there are updates available for the openage converter modpacks. + + :param available_modpacks: Available modpacks and their versions. Modpack names are keys, + versions are values. + :param game_info_dir: The directory containing the game information. + """ + game_editions, game_expansions = create_version_objects(game_info_dir) + for game_def in chain(game_editions, game_expansions): + for targetmod_name, targetmod_def in game_def.target_modpacks.items(): + if targetmod_name in available_modpacks: + converter_version = SemanticVersion(targetmod_def["version"]) + try: + modpack_version = SemanticVersion(available_modpacks[targetmod_name]) + + except ValueError: + # Some really old converted modpacks don't use semantic versioning + # these should always be updated + modpack_version = SemanticVersion("0.0.0") + + if converter_version > modpack_version: + info(("Modpack %s v%s is outdated: " + "newer version v%s is available"), + targetmod_name, modpack_version, converter_version) + + else: + info("Modpack %s v%s is up-to-date", targetmod_name, modpack_version) diff --git a/openage/convert/service/init/conversion_required.py b/openage/convert/service/init/conversion_required.py deleted file mode 100644 index 1c13a33142..0000000000 --- a/openage/convert/service/init/conversion_required.py +++ /dev/null @@ -1,53 +0,0 @@ -# Copyright 2020-2023 the openage authors. See copying.md for legal info. - -""" -Test whether there already are converted modpacks present. -""" -from __future__ import annotations -import typing - -from ....log import info - -from .modpack_search import enumerate_modpacks - -if typing.TYPE_CHECKING: - from openage.util.fslike.union import UnionPath - - -def conversion_required(asset_dir: UnionPath) -> bool: - """ - Check if an asset conversion is required to run the game. - - Asset conversions are required if: - - the modpack folder does not exist - - no modpacks inside the modpack folder exist - - the converted assets are outdated - - :param asset_dir: The asset directory to check. - :type asset_dir: UnionPath - :return: True if an asset conversion is required, else False. - """ - try: - # TODO: Do not use hardcoded path to "converted" directory. - # The mod directory should be its own configurable path - modpacks = enumerate_modpacks(asset_dir / "converted") - - except FileNotFoundError: - # modpack folder not created yet - modpacks = set() - - if not modpacks or \ - len(modpacks) == 1 and "engine" in modpacks: # engine is not usable on its own - info("No converted assets have been found") - return True - - # TODO: Check if modpack version of converted assets are up to date - # if not changes: - # dbg("Converted assets are up to date") - # return False - - # if asset_version >= 0 and asset_version != changelog.ASSET_VERSION: - # info("Found converted assets with version %d, " - # "but need version %d", asset_version, changelog.ASSET_VERSION) - - return False diff --git a/openage/convert/service/init/modpack_search.py b/openage/convert/service/init/modpack_search.py index 8d89b73da1..aa6a814004 100644 --- a/openage/convert/service/init/modpack_search.py +++ b/openage/convert/service/init/modpack_search.py @@ -14,27 +14,35 @@ from openage.util.fslike.union import UnionPath -def enumerate_modpacks(modpacks_dir: UnionPath) -> set[str]: +def enumerate_modpacks(modpacks_dir: UnionPath, exclude: set[str] = None) -> dict[str, str]: """ Enumerate openage modpacks in a directory. :param asset_dir: The asset directory to search in. :type asset_dir: UnionPath - :returns: A list of modpack names that were found. - :rtype: set[str] + :param exclude: Modpack names to exclude from the enumeration. + :type exclude: set[str] + :returns: Modpacks that were found. Names are keys, versions are values. + :rtype: dict[str, str] """ if not modpacks_dir.exists(): info("openage modpack directory has not been created yet") raise FileNotFoundError("openage modpack directory not found") - modpacks: set[str] = set() + modpacks: dict[str] = {} for check_dir in modpacks_dir.iterdir(): if check_dir.is_dir(): try: modpack_info = get_modpack_info(check_dir) modpack_name = modpack_info["info"]["name"] - info("Found modpack %s", modpack_name) - modpacks.add(modpack_name) + modpack_version = modpack_info["info"]["version"] + info("Found modpack %s v%s", modpack_name, modpack_version) + + if exclude is not None and modpack_name in exclude: + dbg("Excluding modpack %s from enumeration", modpack_name) + continue + + modpacks.update({modpack_name: modpack_version}) except (FileNotFoundError, TypeError, toml.TomlDecodeError): dbg("No modpack found in directory: %s", check_dir) @@ -81,7 +89,7 @@ def get_modpack_info(modpack_dir: UnionPath) -> dict[str, typing.Any]: raise err -def query_modpack(proposals: set[str]) -> str: +def query_modpack(proposals: list[str]) -> str: """ Query interactively for a modpack from a selection of proposals. """ diff --git a/openage/convert/service/init/version_detect.py b/openage/convert/service/init/version_detect.py index 792ce9708b..58cec8925a 100644 --- a/openage/convert/service/init/version_detect.py +++ b/openage/convert/service/init/version_detect.py @@ -1,4 +1,4 @@ -# Copyright 2020-2023 the openage authors. See copying.md for legal info. +# Copyright 2020-2024 the openage authors. See copying.md for legal info. # # pylint: disable=too-many-arguments,too-many-locals,too-many-branches """ @@ -180,7 +180,7 @@ def create_game_obj( game_name = game_info['name'] game_id = game_info['game_edition_id'] support = game_info['support'] - modpacks = game_info['targetmod'] + modpacks = game_info['targetmods'] if not expansion: expansions = game_info['expansions'] diff --git a/openage/convert/service/read/gamedata.py b/openage/convert/service/read/gamedata.py index 522d959b8e..0d50c81d03 100644 --- a/openage/convert/service/read/gamedata.py +++ b/openage/convert/service/read/gamedata.py @@ -1,4 +1,4 @@ -# Copyright 2020-2023 the openage authors. See copying.md for legal info. +# Copyright 2020-2024 the openage authors. See copying.md for legal info. """ Module for reading .dat files. @@ -73,8 +73,8 @@ def load_gamespec( # pickle.load() can fail in many ways, we need to catch all. # pylint: disable=broad-except try: - gamespec = pickle.load(cachefile) info("using cached wrapper: %s", cachefile_name) + gamespec = pickle.load(cachefile) return gamespec except Exception: warn("could not use cached wrapper:") diff --git a/openage/convert/tool/driver.py b/openage/convert/tool/driver.py index cdd1ed04a3..61d53b0082 100644 --- a/openage/convert/tool/driver.py +++ b/openage/convert/tool/driver.py @@ -63,9 +63,11 @@ def convert_metadata(args: Namespace) -> None: if gamedata_path.exists(): gamedata_path.removerecursive() - read_start = timeit.default_timer() + # Record time taken for each stage + stages_time = {} # Read .dat + stage_start = timeit.default_timer() debug_gamedata_format(args.debugdir, args.debug_info, args.game_version) gamespec = get_gamespec(args.srcdir, args.game_version, not args.flag("no_pickle_cache")) @@ -84,35 +86,38 @@ def convert_metadata(args: Namespace) -> None: existing_graphics = get_existing_graphics(args) debug_registered_graphics(args.debugdir, args.debug_info, existing_graphics) - read_end = timeit.default_timer() - info("Finished metadata read (%.2f seconds)", read_end - read_start) + stage_end = timeit.default_timer() + info("Finished metadata read (%.2f seconds)", stage_end - stage_start) + stages_time.update({"read": stage_end - stage_start}) - conversion_start = timeit.default_timer() - # Convert + # nyan conversion + stage_start = timeit.default_timer() modpacks = args.converter.convert(gamespec, args, string_resources, existing_graphics) - conversion_end = timeit.default_timer() - info("Finished data conversion (%.2f seconds)", conversion_end - conversion_start) + stage_end = timeit.default_timer() + info("Finished data conversion (%.2f seconds)", stage_end - stage_start) + stages_time.update({"convert": stage_end - stage_start}) - export_start = timeit.default_timer() + # Export modpacks + stage_start = timeit.default_timer() for modpack in modpacks: + mod_export_start = timeit.default_timer() ModpackExporter.export(modpack, args) debug_modpack(args.debugdir, args.debug_info, modpack) - export_end = timeit.default_timer() + mod_export_end = timeit.default_timer() info("Finished export of modpack '%s' v%s (%.2f seconds)", modpack.info.packagename, modpack.info.version, - export_end - export_start) + mod_export_end - mod_export_start) + + stage_end = timeit.default_timer() + info("Finished export (%.2f seconds)", stage_end - stage_start) + stages_time.update({"export": stage_end - stage_start}) - stages_time = { - "read": read_end - read_start, - "convert": conversion_end - conversion_start, - "export": export_end - export_start, - } debug_execution_time(args.debugdir, args.debug_info, stages_time) # TODO: player palettes diff --git a/openage/convert/tool/subtool/acquire_sourcedir.py b/openage/convert/tool/subtool/acquire_sourcedir.py index 5d9368e526..fd070b7825 100644 --- a/openage/convert/tool/subtool/acquire_sourcedir.py +++ b/openage/convert/tool/subtool/acquire_sourcedir.py @@ -1,4 +1,4 @@ -# Copyright 2020-2023 the openage authors. See copying.md for legal info. +# Copyright 2020-2024 the openage authors. See copying.md for legal info. # # pylint: disable=too-many-branches @@ -21,7 +21,6 @@ from urllib.request import urlopen from ....log import warn, info, dbg -from ....util.files import which from ....util.fslike.directory import CaseIgnoringDirectory, Directory if typing.TYPE_CHECKING: @@ -48,13 +47,15 @@ def expand_relative_path(path: str) -> AnyStr: return os.path.realpath(os.path.expandvars(os.path.expanduser(path))) -def wanna_convert() -> bool: +def prompt(msg: str, answer: typing.Union[bool, None] = None) -> bool: """ - Ask the user if assets should be converted. + Ask the user a yes/no question. + + :param msg: Message to display. + :param answer: Pre-determined answer (optional). """ - answer = None while answer is None: - print(" Do you want to convert assets? [Y/n]") + print(f" {msg} [Y/n]") user_selection = input("> ") if user_selection.lower() in {"yes", "y", ""}: @@ -66,88 +67,25 @@ def wanna_convert() -> bool: return answer -def wanna_download_trial() -> bool: +def wanna_convert(answer: typing.Union[bool, None] = None) -> bool: """ - Ask the user if the AoC trial should be downloaded. + Ask the user if assets should be converted. """ - answer = None - while answer is None: - print(" Do you want to download the AoC trial version? [Y/n]") - - user_selection = input("> ") - if user_selection.lower() in {"yes", "y", ""}: - answer = True - - elif user_selection.lower() in {"no", "n"}: - answer = False + return prompt("Do you want to convert assets?", answer=answer) - return answer - -def wanna_use_wine() -> bool: +def wanna_check_updates(answer: typing.Union[bool, None] = None) -> bool: """ - Ask the user if wine should be used. - Wine is not used if user has no wine installed. + Ask the user if they want to check for updates. """ - - # TODO: a possibility to call different wine binaries - # (e.g. wine-devel from wine upstream debian repos) - - if not which("wine"): - return False - - answer = None - long_prompt = True - while answer is None: - if long_prompt: - print( - " Should we call wine to determine an AOE installation? [Y/n]") - long_prompt = False - else: - # TODO: text-adventure here - print(" Don't know what you want. Use wine? [Y/n]") - - user_selection = input("> ") - if user_selection.lower() in {"yes", "y", ""}: - answer = True - - elif user_selection.lower() in {"no", "n"}: - answer = False - - return answer + return prompt("Do you want to check for updates?", answer=answer) -def set_custom_wineprefix() -> None: +def wanna_download_trial(answer: typing.Union[bool, None] = None) -> bool: """ - Allow the customization of the WINEPREFIX environment variable. + Ask the user if the AoC trial should be downloaded. """ - - print("The WINEPREFIX is a separate 'container' for windows " - "software installations.") - - current_wineprefix = os.environ.get("WINEPREFIX") - if current_wineprefix: - print(f"Currently: WINEPREFIX='{current_wineprefix}'") - - print("Enter a custom value or leave empty to keep it as-is:") - while True: - new_wineprefix = input("WINEPREFIX=") - - if not new_wineprefix: - break - - new_wineprefix = expand_relative_path(new_wineprefix) - - # test if it probably is a wineprefix - if (Path(new_wineprefix) / "drive_c").is_dir(): # pylint: disable=no-member - break - - print("This does not appear to be a valid WINEPREFIX.") - print("Enter a valid one, or leave it empty to skip.") - - # store the updated env variable for the wine subprocess - if new_wineprefix: - os.environ["WINEPREFIX"] = new_wineprefix + return prompt("Do you want to download the AoC trial version?", answer=answer) def query_source_dir(proposals: set[str]) -> AnyStr: diff --git a/openage/convert/value_object/init/game_version.py b/openage/convert/value_object/init/game_version.py index e4364c3dde..2bffcbd3ed 100644 --- a/openage/convert/value_object/init/game_version.py +++ b/openage/convert/value_object/init/game_version.py @@ -1,4 +1,4 @@ -# Copyright 2020-2023 the openage authors. See copying.md for legal info. +# Copyright 2020-2024 the openage authors. See copying.md for legal info. # # pylint: disable=too-many-arguments @@ -35,7 +35,7 @@ def __init__( support: Support, game_version_info: list[tuple[list[str], dict[str, str]]], media_paths: list[tuple[str, list[str]]], - modpacks: list[str], + modpacks: dict[str, dict[str, str]], **flags ): """ diff --git a/openage/game/main.py b/openage/game/main.py index 15fb0a3962..0fdf007259 100644 --- a/openage/game/main.py +++ b/openage/game/main.py @@ -1,4 +1,4 @@ -# Copyright 2015-2023 the openage authors. See copying.md for legal info. +# Copyright 2015-2024 the openage authors. See copying.md for legal info. # # pylint: disable=too-many-locals @@ -8,6 +8,7 @@ from __future__ import annotations import typing + from ..log import info if typing.TYPE_CHECKING: @@ -27,9 +28,14 @@ def init_subparser(cli: ArgumentParser) -> None: help="run without displaying graphics") cli.add_argument( - "--modpacks", nargs="+", required=True, + "--modpacks", nargs="+", required=True, type=str, help="list of modpacks to load") + cli.add_argument( + "--check-updates", action='store_true', + help="Check if the assets are up to date" + ) + def main(args, error): """ @@ -43,9 +49,10 @@ def main(args, error): from .main_cpp import run_game from .. import config from ..assets import get_asset_path - from ..convert.main import conversion_required, convert_assets from ..convert.tool.api_export import export_api from ..convert.service.init.api_export_required import api_export_required + from ..convert.service.init.changelog import check_updates + from ..convert.service.init.modpack_search import enumerate_modpacks from ..cppinterface.setup import setup as cpp_interface_setup from ..cvar.location import get_config_path from ..util.fslike.union import Union @@ -77,16 +84,19 @@ def main(args, error): converted_path.mkdirs() export_api(converted_path) - # ensure that the assets have been converted - if conversion_required(asset_path): - convert_assets(asset_path, args) - - # modpacks - mods = [] + # ensure that modpacks are available + modpack_dir = asset_path / "converted" + available_modpacks = enumerate_modpacks(modpack_dir, exclude={"engine"}) for modpack in args.modpacks: - mods.append(modpack.encode("utf-8")) + if modpack not in available_modpacks: + raise FileNotFoundError(f"Modpack '{modpack}' not found in {modpack_dir}") + + # check if the converted modpacks are up to date + if args.check_updates: + check_updates(available_modpacks, args.cfg_dir / "converter" / "games") - args.modpacks = mods + # encode modpacks as bytes for the C++ interface + args.modpacks = [modpack.encode('utf-8') for modpack in args.modpacks] # start the game, continue in main_cpp.pyx! return run_game(args, root) diff --git a/openage/main/main.py b/openage/main/main.py index 685e5b6bc9..fbeb527ee4 100644 --- a/openage/main/main.py +++ b/openage/main/main.py @@ -1,4 +1,4 @@ -# Copyright 2015-2023 the openage authors. See copying.md for legal info. +# Copyright 2015-2024 the openage authors. See copying.md for legal info. """ Main engine entry point for openage. @@ -25,7 +25,7 @@ def init_subparser(cli: ArgumentParser): help="run without displaying graphics") cli.add_argument( - "--modpacks", nargs="+", + "--modpacks", nargs="+", type=str, help="list of modpacks to load") @@ -42,8 +42,10 @@ def main(args, error): from .main_cpp import run_game from .. import config from ..assets import get_asset_path - from ..convert.main import conversion_required, convert_assets + from ..convert.main import convert_assets from ..convert.service.init.api_export_required import api_export_required + from ..convert.service.init.changelog import check_updates + from ..convert.service.init.modpack_search import enumerate_modpacks, query_modpack from ..convert.tool.api_export import export_api from ..convert.tool.subtool.acquire_sourcedir import wanna_convert from ..cppinterface.setup import setup as cpp_interface_setup @@ -77,22 +79,32 @@ def main(args, error): converted_path.mkdirs() export_api(converted_path) - # ensure that the assets have been converted - if wanna_convert() or conversion_required(asset_path): + # check if modpacks need to be converted + if wanna_convert(): convert_assets(asset_path, args) + available_modpacks = enumerate_modpacks(asset_path / "converted", exclude={"engine"}) + if len(available_modpacks) == 0: + info("No modpacks have been found") + if not args.modpacks: + info("Starting bare 'engine' mode") + args.modpacks = ["engine"] + + # check if the converted modpacks are up to date + check_updates(available_modpacks, args.cfg_dir / "converter" / "games") + # pass modpacks to engine if args.modpacks: - mods = [] + # ensure that specified modpacks are available for modpack in args.modpacks: - mods.append(modpack.encode("utf-8")) + if modpack not in available_modpacks: + raise FileNotFoundError( + f"Modpack '{modpack}' not found in {asset_path / 'converted'}") - args.modpacks = mods + args.modpacks = [modpack.encode("utf-8") for modpack in args.modpacks] else: - from ..convert.service.init.modpack_search import enumerate_modpacks, query_modpack - avail_modpacks = enumerate_modpacks(asset_path / "converted") - args.modpacks = [query_modpack(avail_modpacks).encode("utf-8")] + args.modpacks = [query_modpack(list(available_modpacks.keys())).encode("utf-8")] # start the game, continue in main_cpp.pyx! return run_game(args, root) diff --git a/openage/util/CMakeLists.txt b/openage/util/CMakeLists.txt index 79e7309cb3..cc31682da6 100644 --- a/openage/util/CMakeLists.txt +++ b/openage/util/CMakeLists.txt @@ -16,6 +16,7 @@ add_py_modules( struct.py system.py threading.py + version.py ) add_subdirectory(filelike) diff --git a/openage/util/version.py b/openage/util/version.py new file mode 100644 index 0000000000..712b04ec79 --- /dev/null +++ b/openage/util/version.py @@ -0,0 +1,84 @@ +# Copyright 2024-2024 the openage authors. See copying.md for legal info. + +""" +Handling of version information for openage. +""" +from __future__ import annotations + +import re + +SEMVER_REGEX = re.compile( + (r"^(?P0|[1-9]\d*)\.(?P0|[1-9]\d*)\.(?P0|[1-9]\d*)" + r"(?:-(?P(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)" + r"(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?" + r"(?:\+(?P[0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$")) + + +class SemanticVersion: + """ + Semantic versioning information. + """ + + def __init__(self, version: str) -> None: + """ + Create a new semantic version object from a version string. + + :param version: The version string to parse. + """ + match = SEMVER_REGEX.match(version) + if not match: + raise ValueError(f"Invalid semantic version: {version}") + + self.major = int(match.group("major")) + self.minor = int(match.group("minor")) + self.patch = int(match.group("patch")) + self.prerelease = match.group("prerelease") + self.buildmetadata = match.group("buildmetadata") + + def __lt__(self, other: SemanticVersion) -> bool: + if self.major < other.major: + return True + if self.minor < other.minor: + return True + if self.patch < other.patch: + return True + + return False + + def __le__(self, other: SemanticVersion) -> bool: + if self.major <= other.major: + return True + + if self.minor <= other.minor: + return True + + if self.patch <= other.patch: + return True + + return False + + def __eq__(self, other: SemanticVersion) -> bool: + return (self.major == other.major and + self.minor == other.minor and + self.patch == other.patch) + + def __ne__(self, other: SemanticVersion) -> bool: + return not self.__eq__(other) + + def __gt__(self, other: SemanticVersion) -> bool: + return not self.__le__(other) + + def __ge__(self, other: SemanticVersion) -> bool: + return not self.__lt__(other) + + def __str__(self) -> str: + version = f"{self.major}.{self.minor}.{self.patch}" + if self.prerelease: + version += f"-{self.prerelease}" + if self.buildmetadata: + version += f"+{self.buildmetadata}" + + return version + + def __repr__(self) -> str: + return f"SemanticVersion('{str(self)}')"