mirror of
https://github.com/MarioSpore/Grinch-AP.git
synced 2025-10-21 20:21:32 -06:00

* Render option documentation as reStructuredText in the WebView This means that options can use the standard Python documentation format, while producing much nicer-looking documentation in the WebView with things like emphasis, lists, and so on. * Opt existing worlds out of rich option docs This avoids breaking the rendering of existing option docs which were written with the old plain text rendering in mind, while also allowing new options to default to the rich text rendering instead. * Use reStructuredText formatting for Lingo Options docstrings * Disable raw and file insertion RST directives * Update doc comments per code review * Make rich text docs opt-in * Put rich_text_options_doc on WebWorld * Document rich text API * Code review * Update docs/options api.md Co-authored-by: Doug Hoskisson <beauxq@users.noreply.github.com> * Update Options.py Co-authored-by: Doug Hoskisson <beauxq@users.noreply.github.com> --------- Co-authored-by: Chris Wilson <chris@legendserver.info> Co-authored-by: Doug Hoskisson <beauxq@users.noreply.github.com>
132 lines
4.8 KiB
Python
132 lines
4.8 KiB
Python
import importlib
|
|
import importlib.util
|
|
import logging
|
|
import os
|
|
import sys
|
|
import warnings
|
|
import zipimport
|
|
import time
|
|
import dataclasses
|
|
from typing import Dict, List, TypedDict
|
|
|
|
from Utils import local_path, user_path
|
|
|
|
local_folder = os.path.dirname(__file__)
|
|
user_folder = user_path("worlds") if user_path() != local_path() else user_path("custom_worlds")
|
|
try:
|
|
os.makedirs(user_folder, exist_ok=True)
|
|
except OSError: # can't access/write?
|
|
user_folder = None
|
|
|
|
__all__ = {
|
|
"network_data_package",
|
|
"AutoWorldRegister",
|
|
"world_sources",
|
|
"local_folder",
|
|
"user_folder",
|
|
"GamesPackage",
|
|
"DataPackage",
|
|
"failed_world_loads",
|
|
}
|
|
|
|
|
|
failed_world_loads: List[str] = []
|
|
|
|
|
|
class GamesPackage(TypedDict, total=False):
|
|
item_name_groups: Dict[str, List[str]]
|
|
item_name_to_id: Dict[str, int]
|
|
location_name_groups: Dict[str, List[str]]
|
|
location_name_to_id: Dict[str, int]
|
|
checksum: str
|
|
|
|
|
|
class DataPackage(TypedDict):
|
|
games: Dict[str, GamesPackage]
|
|
|
|
|
|
@dataclasses.dataclass(order=True)
|
|
class WorldSource:
|
|
path: str # typically relative path from this module
|
|
is_zip: bool = False
|
|
relative: bool = True # relative to regular world import folder
|
|
time_taken: float = -1.0
|
|
|
|
def __repr__(self) -> str:
|
|
return f"{self.__class__.__name__}({self.path}, is_zip={self.is_zip}, relative={self.relative})"
|
|
|
|
@property
|
|
def resolved_path(self) -> str:
|
|
if self.relative:
|
|
return os.path.join(local_folder, self.path)
|
|
return self.path
|
|
|
|
def load(self) -> bool:
|
|
try:
|
|
start = time.perf_counter()
|
|
if self.is_zip:
|
|
importer = zipimport.zipimporter(self.resolved_path)
|
|
if hasattr(importer, "find_spec"): # new in Python 3.10
|
|
spec = importer.find_spec(os.path.basename(self.path).rsplit(".", 1)[0])
|
|
assert spec, f"{self.path} is not a loadable module"
|
|
mod = importlib.util.module_from_spec(spec)
|
|
else: # TODO: remove with 3.8 support
|
|
mod = importer.load_module(os.path.basename(self.path).rsplit(".", 1)[0])
|
|
|
|
mod.__package__ = f"worlds.{mod.__package__}"
|
|
mod.__name__ = f"worlds.{mod.__name__}"
|
|
sys.modules[mod.__name__] = mod
|
|
with warnings.catch_warnings():
|
|
warnings.filterwarnings("ignore", message="__package__ != __spec__.parent")
|
|
# Found no equivalent for < 3.10
|
|
if hasattr(importer, "exec_module"):
|
|
importer.exec_module(mod)
|
|
else:
|
|
importlib.import_module(f".{self.path}", "worlds")
|
|
self.time_taken = time.perf_counter()-start
|
|
return True
|
|
|
|
except Exception:
|
|
# A single world failing can still mean enough is working for the user, log and carry on
|
|
import traceback
|
|
import io
|
|
file_like = io.StringIO()
|
|
print(f"Could not load world {self}:", file=file_like)
|
|
traceback.print_exc(file=file_like)
|
|
file_like.seek(0)
|
|
logging.exception(file_like.read())
|
|
failed_world_loads.append(os.path.basename(self.path).rsplit(".", 1)[0])
|
|
return False
|
|
|
|
|
|
# find potential world containers, currently folders and zip-importable .apworld's
|
|
world_sources: List[WorldSource] = []
|
|
for folder in (folder for folder in (user_folder, local_folder) if folder):
|
|
relative = folder == local_folder
|
|
for entry in os.scandir(folder):
|
|
# prevent loading of __pycache__ and allow _* for non-world folders, disable files/folders starting with "."
|
|
if not entry.name.startswith(("_", ".")):
|
|
file_name = entry.name if relative else os.path.join(folder, entry.name)
|
|
if entry.is_dir():
|
|
if os.path.isfile(os.path.join(entry.path, '__init__.py')):
|
|
world_sources.append(WorldSource(file_name, relative=relative))
|
|
elif os.path.isfile(os.path.join(entry.path, '__init__.pyc')):
|
|
world_sources.append(WorldSource(file_name, relative=relative))
|
|
else:
|
|
logging.warning(f"excluding {entry.name} from world sources because it has no __init__.py")
|
|
elif entry.is_file() and entry.name.endswith(".apworld"):
|
|
world_sources.append(WorldSource(file_name, is_zip=True, relative=relative))
|
|
|
|
# import all submodules to trigger AutoWorldRegister
|
|
world_sources.sort()
|
|
for world_source in world_sources:
|
|
world_source.load()
|
|
|
|
# Build the data package for each game.
|
|
from .AutoWorld import AutoWorldRegister
|
|
|
|
network_data_package: DataPackage = {
|
|
"games": {world_name: world.get_data_package_data() for world_name, world in AutoWorldRegister.world_types.items()},
|
|
}
|
|
|