diff --git a/Generate.py b/Generate.py index 5d65a688..8e813203 100644 --- a/Generate.py +++ b/Generate.py @@ -486,7 +486,22 @@ def roll_settings(weights: dict, plando_options: PlandoOptions = PlandoOptions.b if required_plando_options: raise Exception(f"Settings reports required plando module {str(required_plando_options)}, " f"which is not enabled.") - + games = requirements.get("game", {}) + for game, version in games.items(): + if game not in AutoWorldRegister.world_types: + continue + if not version: + raise Exception(f"Invalid version for game {game}: {version}.") + if isinstance(version, str): + version = {"min": version} + if "min" in version and tuplize_version(version["min"]) > AutoWorldRegister.world_types[game].world_version: + raise Exception(f"Settings reports required version of world \"{game}\" is at least {version['min']}, " + f"however world is of version " + f"{AutoWorldRegister.world_types[game].world_version.as_simple_string()}.") + if "max" in version and tuplize_version(version["max"]) < AutoWorldRegister.world_types[game].world_version: + raise Exception(f"Settings reports required version of world \"{game}\" is no later than {version['max']}, " + f"however world is of version " + f"{AutoWorldRegister.world_types[game].world_version.as_simple_string()}.") ret = argparse.Namespace() for option_key in Options.PerGameCommonOptions.type_hints: if option_key in weights and option_key not in Options.CommonOptions.type_hints: diff --git a/Main.py b/Main.py index 6d81ff23..d872a3c1 100644 --- a/Main.py +++ b/Main.py @@ -59,7 +59,9 @@ def main(args, seed=None, baked_server_options: dict[str, object] | None = None) for name, cls in AutoWorld.AutoWorldRegister.world_types.items(): if not cls.hidden and len(cls.item_names) > 0: - logger.info(f" {name:{longest_name}}: Items: {len(cls.item_names):{item_count}} | " + logger.info(f" {name:{longest_name}}: " + f"v{cls.world_version.as_simple_string()} |" + f"Items: {len(cls.item_names):{item_count}} | " f"Locations: {len(cls.location_names):{location_count}}") del item_count, location_count diff --git a/Options.py b/Options.py index dc1e8c90..282f7576 100644 --- a/Options.py +++ b/Options.py @@ -1710,7 +1710,7 @@ def generate_yaml_templates(target_folder: typing.Union[str, "pathlib.Path"], ge from jinja2 import Template from worlds import AutoWorldRegister - from Utils import local_path, __version__ + from Utils import local_path, __version__, tuplize_version full_path: str @@ -1753,7 +1753,10 @@ def generate_yaml_templates(target_folder: typing.Union[str, "pathlib.Path"], ge res = template.render( option_groups=option_groups, - __version__=__version__, game=game_name, yaml_dump=yaml_dump_scalar, + __version__=__version__, + game=game_name, + world_version=world.world_version.as_simple_string(), + yaml_dump=yaml_dump_scalar, dictify_range=dictify_range, cleandoc=cleandoc, ) diff --git a/data/options.yaml b/data/options.yaml index f2621124..3278a3c5 100644 --- a/data/options.yaml +++ b/data/options.yaml @@ -33,6 +33,10 @@ description: {{ yaml_dump("Default %s Template" % game) }} game: {{ yaml_dump(game) }} requires: version: {{ __version__ }} # Version of Archipelago required for this yaml to work as expected. + {%- if world_version != "0.0.0" %} + game: + {{ yaml_dump(game) }}: {{ world_version }} # Version of the world required for this yaml to work as expected. + {%- endif %} {%- macro range_option(option) %} # You can define additional values between the minimum and maximum values. diff --git a/worlds/AutoWorld.py b/worlds/AutoWorld.py index 676171b7..1805b11a 100644 --- a/worlds/AutoWorld.py +++ b/worlds/AutoWorld.py @@ -12,7 +12,7 @@ from typing import (Any, Callable, ClassVar, Dict, FrozenSet, Iterable, List, Ma from Options import item_and_loc_options, ItemsAccessibility, OptionGroup, PerGameCommonOptions from BaseClasses import CollectionState -from Utils import deprecate +from Utils import Version if TYPE_CHECKING: from BaseClasses import MultiWorld, Item, Location, Tutorial, Region, Entrance @@ -75,6 +75,10 @@ class AutoWorldRegister(type): if "required_client_version" in base.__dict__: dct["required_client_version"] = max(dct["required_client_version"], base.__dict__["required_client_version"]) + if "world_version" in dct: + if dct["world_version"] != Version(0, 0, 0): + raise RuntimeError(f"{name} is attempting to set 'world_version' from within the class. world_version " + f"can only be set from manifest.") # construct class new_class = super().__new__(mcs, name, bases, dct) @@ -337,6 +341,8 @@ class World(metaclass=AutoWorldRegister): """If loaded from a .apworld, this is the Path to it.""" __file__: ClassVar[str] """path it was loaded from""" + world_version: ClassVar[Version] = Version(0, 0, 0) + """Optional world version loaded from archipelago.json""" def __init__(self, multiworld: "MultiWorld", player: int): assert multiworld is not None diff --git a/worlds/__init__.py b/worlds/__init__.py index c363d7f2..b9ef225f 100644 --- a/worlds/__init__.py +++ b/worlds/__init__.py @@ -7,10 +7,11 @@ import warnings import zipimport import time import dataclasses +import json from typing import List from NetUtils import DataPackage -from Utils import local_path, user_path, Version, version_tuple +from Utils import local_path, user_path, Version, version_tuple, tuplize_version local_folder = os.path.dirname(__file__) user_folder = user_path("worlds") if user_path() != local_path() else user_path("custom_worlds") @@ -111,8 +112,25 @@ for world_source in world_sources: else: world_source.load() + from .AutoWorld import AutoWorldRegister +for world_source in world_sources: + if not world_source.is_zip: + # look for manifest + manifest = {} + for dirpath, dirnames, filenames in os.walk(world_source.resolved_path): + for file in filenames: + if file.endswith("archipelago.json"): + manifest = json.load(open(os.path.join(dirpath, file), "r")) + break + if manifest: + break + game = manifest.get("game") + if game in AutoWorldRegister.world_types: + AutoWorldRegister.world_types[game].world_version = Version(*tuplize_version(manifest.get("world_version", + "0.0.0"))) + if apworlds: # encapsulation for namespace / gc purposes def load_apworlds() -> None: @@ -164,6 +182,10 @@ if apworlds: add_as_failed_to_load=False) else: apworld_source.load() + if apworld.game in AutoWorldRegister.world_types: + # world could fail to load at this point + if apworld.world_version: + AutoWorldRegister.world_types[apworld.game].world_version = apworld.world_version load_apworlds() del load_apworlds diff --git a/worlds/generic/docs/advanced_settings_en.md b/worlds/generic/docs/advanced_settings_en.md index 25943076..bc8754b9 100644 --- a/worlds/generic/docs/advanced_settings_en.md +++ b/worlds/generic/docs/advanced_settings_en.md @@ -81,7 +81,8 @@ are `description`, `name`, `game`, `requires`, and the name of the games you wan * `requires` details different requirements from the generator for the YAML to work as you expect it to. Generally this is good for detailing the version of Archipelago this YAML was prepared for. If it is rolled on an older version, options may be missing and as such it will not work as expected. If any plando is used in the file then requiring it - here to ensure it will be used is good practice. + here to ensure it will be used is good practice. Specific versions of custom worlds can also be required, ensuring + that the generator is using a compatible version. ## Game Options @@ -165,7 +166,9 @@ game: A Link to the Past: 10 Timespinner: 10 requires: - version: 0.4.1 + version: 0.6.4 + game: + A Link to the Past: 0.6.4 A Link to the Past: accessibility: minimal progression_balancing: 50 @@ -229,7 +232,7 @@ Timespinner: * `name` is `Example Player` and this will be used in the server console when sending and receiving items. * `game` has an equal chance of being either `A Link to the Past` or `Timespinner` with a 10/20 chance for each. This is because each game has a weight of 10 and the total of all weights is 20. -* `requires` is set to required release version 0.3.2 or higher. +* `requires` is set to require Archipelago release version 0.6.4 or higher, as well as A Link to the Past version 0.6.4. * `accessibility` for both games is set to `minimal` which will set this seed to beatable only, so some locations and items may be completely inaccessible but the seed will still be completable. * `progression_balancing` for both games is set to 50, the default value, meaning we will likely receive important items diff --git a/worlds/mm2/__init__.py b/worlds/mm2/__init__.py index 529810d4..5389fc8a 100644 --- a/worlds/mm2/__init__.py +++ b/worlds/mm2/__init__.py @@ -96,7 +96,6 @@ class MM2World(World): location_name_groups = location_groups web = MM2WebWorld() rom_name: bytearray - world_version: Tuple[int, int, int] = (0, 3, 2) wily_5_weapons: Dict[int, List[int]] def __init__(self, multiworld: MultiWorld, player: int): diff --git a/worlds/mm2/archipelago.json b/worlds/mm2/archipelago.json new file mode 100644 index 00000000..75c098fd --- /dev/null +++ b/worlds/mm2/archipelago.json @@ -0,0 +1,5 @@ +{ + "game": "Mega Man 2", + "world_version": "0.3.2", + "minimum_ap_version": "0.6.4" +}