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:
17
Generate.py
17
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:
|
||||
|
||||
4
Main.py
4
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
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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):
|
||||
|
||||
5
worlds/mm2/archipelago.json
Normal file
5
worlds/mm2/archipelago.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"game": "Mega Man 2",
|
||||
"world_version": "0.3.2",
|
||||
"minimum_ap_version": "0.6.4"
|
||||
}
|
||||
Reference in New Issue
Block a user