Core: allow loading worlds from zip modules (#747)

* Core: allow loading worlds from zip modules
RoR2: make it zipimport compatible (remove relative imports beyond local top-level)

* WebHost: add support for .apworld
This commit is contained in:
Fabian Dill
2022-08-15 23:52:03 +02:00
committed by GitHub
parent 086295adbb
commit ca83905d9f
6 changed files with 104 additions and 46 deletions

View File

@@ -2,10 +2,13 @@ from __future__ import annotations
import logging
import sys
from typing import Dict, FrozenSet, Set, Tuple, List, Optional, TextIO, Any, Callable, Union, NamedTuple
from typing import Dict, FrozenSet, Set, Tuple, List, Optional, TextIO, Any, Callable, Union, TYPE_CHECKING
from BaseClasses import MultiWorld, Item, CollectionState, Location, Tutorial
from Options import Option
from BaseClasses import CollectionState
if TYPE_CHECKING:
from BaseClasses import MultiWorld, Item, Location, Tutorial
class AutoWorldRegister(type):
@@ -41,8 +44,11 @@ class AutoWorldRegister(type):
# construct class
new_class = super().__new__(mcs, name, bases, dct)
if "game" in dct:
if dct["game"] in AutoWorldRegister.world_types:
raise RuntimeError(f"""Game {dct["game"]} already registered.""")
AutoWorldRegister.world_types[dct["game"]] = new_class
new_class.__file__ = sys.modules[new_class.__module__].__file__
new_class.is_zip = ".apworld" in new_class.__file__
return new_class
@@ -62,12 +68,12 @@ class AutoLogicRegister(type):
return new_class
def call_single(world: MultiWorld, method_name: str, player: int, *args: Any) -> Any:
def call_single(world: "MultiWorld", method_name: str, player: int, *args: Any) -> Any:
method = getattr(world.worlds[player], method_name)
return method(*args)
def call_all(world: MultiWorld, method_name: str, *args: Any) -> None:
def call_all(world: "MultiWorld", method_name: str, *args: Any) -> None:
world_types: Set[AutoWorldRegister] = set()
for player in world.player_ids:
world_types.add(world.worlds[player].__class__)
@@ -79,7 +85,7 @@ def call_all(world: MultiWorld, method_name: str, *args: Any) -> None:
stage_callable(world, *args)
def call_stage(world: MultiWorld, method_name: str, *args: Any) -> None:
def call_stage(world: "MultiWorld", method_name: str, *args: Any) -> None:
world_types = {world.worlds[player].__class__ for player in world.player_ids}
for world_type in world_types:
stage_callable = getattr(world_type, f"stage_{method_name}", None)
@@ -97,7 +103,7 @@ class WebWorld:
# docs folder will also be scanned for tutorial guides given the relevant information in this list. Each Tutorial
# class is to be used for one guide.
tutorials: List[Tutorial]
tutorials: List["Tutorial"]
# Choose a theme for your /game/* pages
# Available: dirt, grass, grassFlowers, ice, jungle, ocean, partyTime, stone
@@ -159,8 +165,11 @@ class World(metaclass=AutoWorldRegister):
# Hide World Type from various views. Does not remove functionality.
hidden: bool = False
# see WebWorld for options
web: WebWorld = WebWorld()
# autoset on creation:
world: MultiWorld
world: "MultiWorld"
player: int
# automatically generated
@@ -170,9 +179,10 @@ class World(metaclass=AutoWorldRegister):
item_names: Set[str] # set of all potential item names
location_names: Set[str] # set of all potential location names
web: WebWorld = WebWorld()
is_zip: bool # was loaded from a .apworld ?
__file__: str # path it was loaded from
def __init__(self, world: MultiWorld, player: int):
def __init__(self, world: "MultiWorld", player: int):
self.world = world
self.player = player
@@ -207,12 +217,12 @@ class World(metaclass=AutoWorldRegister):
@classmethod
def fill_hook(cls,
progitempool: List[Item],
nonexcludeditempool: List[Item],
localrestitempool: Dict[int, List[Item]],
nonlocalrestitempool: Dict[int, List[Item]],
restitempool: List[Item],
fill_locations: List[Location]) -> None:
progitempool: List["Item"],
nonexcludeditempool: List["Item"],
localrestitempool: Dict[int, List["Item"]],
nonlocalrestitempool: Dict[int, List["Item"]],
restitempool: List["Item"],
fill_locations: List["Location"]) -> None:
"""Special method that gets called as part of distribute_items_restrictive (main fill).
This gets called once per present world type."""
pass
@@ -250,7 +260,7 @@ class World(metaclass=AutoWorldRegister):
# end of ordered Main.py calls
def create_item(self, name: str) -> Item:
def create_item(self, name: str) -> "Item":
"""Create an item for this world type and player.
Warning: this may be called with self.world = None, for example by MultiServer"""
raise NotImplementedError
@@ -261,7 +271,7 @@ class World(metaclass=AutoWorldRegister):
return self.world.random.choice(tuple(self.item_name_to_id.keys()))
# decent place to implement progressive items, in most cases can stay as-is
def collect_item(self, state: CollectionState, item: Item, remove: bool = False) -> Optional[str]:
def collect_item(self, state: "CollectionState", item: "Item", remove: bool = False) -> Optional[str]:
"""Collect an item name into state. For speed reasons items that aren't logically useful get skipped.
Collect None to skip item.
:param state: CollectionState to collect into
@@ -272,18 +282,18 @@ class World(metaclass=AutoWorldRegister):
return None
# called to create all_state, return Items that are created during pre_fill
def get_pre_fill_items(self) -> List[Item]:
def get_pre_fill_items(self) -> List["Item"]:
return []
# following methods should not need to be overridden.
def collect(self, state: CollectionState, item: Item) -> bool:
def collect(self, state: "CollectionState", item: "Item") -> bool:
name = self.collect_item(state, item)
if name:
state.prog_items[name, self.player] += 1
return True
return False
def remove(self, state: CollectionState, item: Item) -> bool:
def remove(self, state: "CollectionState", item: "Item") -> bool:
name = self.collect_item(state, item, True)
if name:
state.prog_items[name, self.player] -= 1
@@ -292,7 +302,7 @@ class World(metaclass=AutoWorldRegister):
return True
return False
def create_filler(self) -> Item:
def create_filler(self) -> "Item":
return self.create_item(self.get_filler_item_name())