Compare commits

..

3 Commits

Author SHA1 Message Date
Berserker
df76c26fbb Core: add assertion preventing building with empty platforms list 2026-03-08 21:21:39 +01:00
Berserker
9a900e29e5 Core: any platform is now None/missing key 2026-03-08 17:26:12 +01:00
Berserker
41eba5a2f6 Core: add platforms field to manifest 2026-03-05 00:38:16 +01:00
8 changed files with 28 additions and 49 deletions

View File

@@ -32,6 +32,8 @@ If the APWorld is a folder, the only required field is "game":
There are also the following optional fields:
* `minimum_ap_version` and `maximum_ap_version` - which if present will each be compared against the current
Archipelago version respectively to filter those files from being loaded.
* `platforms` - a list of strings indicating the `sys.platform`(s) the world can run on.
If empty or not set, it is assumed to be any that python itself can run on.
* `world_version` - an arbitrary version for that world in order to only load the newest valid world.
An APWorld without a world_version is always treated as older than one with a version
(**Must** use exactly the format `"major.minor.build"`, e.g. `1.0.0`)

View File

@@ -409,6 +409,7 @@ class BuildExeCommand(cx_Freeze.command.build_exe.build_exe):
apworld = APWorldContainer(str(zip_path))
apworld.minimum_ap_version = version_tuple
apworld.maximum_ap_version = version_tuple
apworld.platforms = [sys.platform]
apworld.game = worldtype.game
manifest.update(apworld.get_manifest())
apworld.manifest_path = f"{file_name}/archipelago.json"

View File

@@ -13,7 +13,7 @@ from typing import (Any, ClassVar, Dict, FrozenSet, List, Optional, Self, Set, T
from Options import item_and_loc_options, ItemsAccessibility, OptionGroup, PerGameCommonOptions
from BaseClasses import CollectionState, Entrance
from rule_builder.rules import CustomRuleRegister, Rule
from Utils import Version, tuplize_version, version_tuple
from Utils import Version
if TYPE_CHECKING:
from BaseClasses import CollectionRule, Item, Location, MultiWorld, Region, Tutorial
@@ -33,7 +33,6 @@ class AutoWorldRegister(type):
zip_path: Optional[str]
settings_key: str
__settings: Any
__manifest: Any
@property
def settings(cls) -> Any: # actual type is defined in World
@@ -46,38 +45,6 @@ class AutoWorldRegister(type):
return None
return cls.__settings
@property
def _manifest(cls) -> Dict[str, Any]:
if cls.__manifest is None:
if cls.zip_path:
import zipfile
from .Files import APWorldContainer
container = APWorldContainer(str(cls.zip_path))
with zipfile.ZipFile(container.path, "r") as zf:
cls.__manifest = container.read_contents(zf)
else:
import json
import os
# look for manifest
manifest_path = None
world_dir = pathlib.Path(cls.__file__).parent
for dirpath, dirnames, filenames in os.walk(world_dir):
for file in filenames:
if file.endswith("archipelago.json"):
manifest_path = os.path.join(dirpath, file)
break
if manifest_path:
break
if manifest_path:
with open(manifest_path, "r", encoding="utf-8-sig") as f:
cls.__manifest = json.load(f)
elif version_tuple < (0, 7, 0):
cls.__manifest = {}
else:
raise RuntimeError(f"Could not find manifest for {cls.__name__} in {world_dir}.")
return cls.__manifest
def __new__(mcs, name: str, bases: Tuple[type, ...], dct: Dict[str, Any]) -> AutoWorldRegister:
if "web" in dct:
assert isinstance(dct["web"], WebWorld), "WebWorld has to be instantiated."
@@ -134,7 +101,6 @@ class AutoWorldRegister(type):
world_folder_name = mod_name[7:].lower() if mod_name.startswith("worlds.") else mod_name.lower()
new_class.settings_key = world_folder_name + "_options"
new_class.__settings = None
new_class.__manifest = None
return new_class
@@ -387,6 +353,8 @@ class World(metaclass=AutoWorldRegister):
"""path it was loaded from"""
world_version: ClassVar[Version] = Version(0, 0, 0)
"""Optional world version loaded from archipelago.json"""
platforms: ClassVar[Optional[List[str]]] = None
"""Optional platforms loaded from archipelago.json"""
def __init__(self, multiworld: "MultiWorld", player: int):
assert multiworld is not None
@@ -396,15 +364,10 @@ class World(metaclass=AutoWorldRegister):
multiworld.per_slot_randoms[player] = self.random
def __getattr__(self, item: str) -> Any:
if item in ("settings", "_manifest"):
return getattr(self.__class__, item)
if item == "settings":
return self.__class__.settings
raise AttributeError
@property
def version(self) -> Version:
"""World version loaded from archipelago.json"""
return tuplize_version(self._manifest.get("world_version", "0.0.0"))
# overridable methods that get called by Main.py, sorted by execution order
# can also be implemented as a classmethod and called "stage_<original_name>",
# in that case the MultiWorld object is passed as the first argument, and it gets called once for the entire multiworld.

View File

@@ -197,6 +197,7 @@ class APWorldContainer(APContainer):
world_version: "Version | None" = None
minimum_ap_version: "Version | None" = None
maximum_ap_version: "Version | None" = None
platforms: Optional[List[str]] = None
def read_contents(self, opened_zipfile: zipfile.ZipFile) -> Dict[str, Any]:
from Utils import tuplize_version
@@ -205,6 +206,7 @@ class APWorldContainer(APContainer):
for version_key in ("world_version", "minimum_ap_version", "maximum_ap_version"):
if version_key in manifest:
setattr(self, version_key, tuplize_version(manifest[version_key]))
self.platforms = manifest.get("platforms")
return manifest
def get_manifest(self) -> Dict[str, Any]:
@@ -215,6 +217,8 @@ class APWorldContainer(APContainer):
version = getattr(self, version_key)
if version:
manifest[version_key] = version.as_simple_string()
if self.platforms:
manifest["platforms"] = self.platforms
return manifest

View File

@@ -289,6 +289,12 @@ if not is_frozen():
if not worldtype:
logging.error(f"Requested APWorld \"{worldname}\" does not exist.")
continue
assert worldtype.platforms != [], (
f"World {worldname} has an empty list for platforms. "
"Use None or omit the attribute for 'any platform'."
)
file_name = os.path.split(os.path.dirname(worldtype.__file__))[1]
world_directory = os.path.join("worlds", file_name)
if os.path.isfile(os.path.join(world_directory, "archipelago.json")):

View File

@@ -118,6 +118,7 @@ for world_source in world_sources:
game = manifest.get("game")
if game in AutoWorldRegister.world_types:
AutoWorldRegister.world_types[game].world_version = tuplize_version(manifest.get("world_version", "0.0.0"))
AutoWorldRegister.world_types[game].platforms = manifest.get("platforms")
if apworlds:
# encapsulation for namespace / gc purposes
@@ -165,6 +166,11 @@ if apworlds:
f"Did not load {apworld_source.path} "
f"as its maximum core version {apworld.maximum_ap_version} "
f"is lower than current core version {version_tuple}.")
elif apworld.platforms and sys.platform not in apworld.platforms:
fail_world(apworld.game,
f"Did not load {apworld_source.path} "
f"as it is not compatible with current platform {sys.platform}. "
f"Supported platforms: {', '.join(apworld.platforms)}")
else:
core_compatible.append((apworld_source, apworld))
# load highest version first
@@ -199,6 +205,8 @@ if apworlds:
# world could fail to load at this point
if apworld.world_version:
AutoWorldRegister.world_types[apworld.game].world_version = apworld.world_version
if apworld.platforms:
AutoWorldRegister.world_types[apworld.game].platforms = apworld.platforms
load_apworlds()
del load_apworlds

View File

@@ -1699,7 +1699,8 @@ def patch_rom(multiworld: MultiWorld, rom: LocalRom, player: int, enemized: bool
# set rom name
# 21 bytes
rom.name = bytearray(f'AP{local_world.version.as_simple_string().replace(".", "")[0:3]}_{player}_{multiworld.seed:11}\0', 'utf8')[:21]
from Utils import __version__
rom.name = bytearray(f'AP{__version__.replace(".", "")[0:3]}_{player}_{multiworld.seed:11}\0', 'utf8')[:21]
rom.name.extend([0] * (21 - len(rom.name)))
rom.write_bytes(0x7FC0, rom.name)

View File

@@ -1,6 +0,0 @@
{
"game": "A Link to the Past",
"minimum_ap_version": "0.6.6",
"world_version": "5.1.0",
"authors": ["Berserker"]
}