Core: expose world version to world classes and yaml (#5484)

* support version on new manifest

* apply world version from manifest

* Update Generate.py

* docs

* reduce mm2 version again

* wrong version

* validate game in world_types

* Update Generate.py

* let unknown game fall through to later exception

* hide real world version behind property

* named tuple is immutable

* write minimum world version to template yaml, fix gen edge cases

* punctuation

* check for world version in autoworldregister

* missed one

---------

Co-authored-by: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com>
This commit is contained in:
Silvris
2025-09-30 19:47:08 -05:00
committed by GitHub
parent 4893ac3e51
commit 33b485c0c3
9 changed files with 69 additions and 10 deletions

View File

@@ -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:

View File

@@ -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

View File

@@ -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,
)

View File

@@ -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.

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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):

View File

@@ -0,0 +1,5 @@
{
"game": "Mega Man 2",
"world_version": "0.3.2",
"minimum_ap_version": "0.6.4"
}