2021-06-29 03:49:29 +02:00
|
|
|
import importlib
|
2024-06-08 20:04:17 +02:00
|
|
|
import importlib.util
|
2024-06-06 11:36:14 -07:00
|
|
|
import logging
|
2021-06-29 03:49:29 +02:00
|
|
|
import os
|
2022-10-01 02:47:31 +02:00
|
|
|
import sys
|
|
|
|
|
import warnings
|
|
|
|
|
import zipimport
|
2024-02-04 09:09:07 +01:00
|
|
|
import time
|
|
|
|
|
import dataclasses
|
2025-07-26 16:27:29 -07:00
|
|
|
from typing import List
|
2021-02-26 21:03:16 +01:00
|
|
|
|
2025-07-26 16:27:29 -07:00
|
|
|
from NetUtils import DataPackage
|
2025-09-24 02:39:19 +02:00
|
|
|
from Utils import local_path, user_path, Version, version_tuple
|
2022-08-15 23:52:03 +02:00
|
|
|
|
2023-11-04 10:26:51 +01:00
|
|
|
local_folder = os.path.dirname(__file__)
|
2024-06-06 01:36:02 +02:00
|
|
|
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
|
2023-11-04 10:26:51 +01:00
|
|
|
|
2023-11-18 12:29:35 -06:00
|
|
|
__all__ = {
|
2022-08-15 23:52:03 +02:00
|
|
|
"network_data_package",
|
|
|
|
|
"AutoWorldRegister",
|
|
|
|
|
"world_sources",
|
2023-11-04 10:26:51 +01:00
|
|
|
"local_folder",
|
|
|
|
|
"user_folder",
|
2024-03-28 22:21:56 +01:00
|
|
|
"failed_world_loads",
|
2023-11-18 12:29:35 -06:00
|
|
|
}
|
2022-11-28 02:25:53 +01:00
|
|
|
|
|
|
|
|
|
2024-03-28 22:21:56 +01:00
|
|
|
failed_world_loads: List[str] = []
|
|
|
|
|
|
|
|
|
|
|
2024-02-04 09:09:07 +01:00
|
|
|
@dataclasses.dataclass(order=True)
|
|
|
|
|
class WorldSource:
|
2022-08-15 23:52:03 +02:00
|
|
|
path: str # typically relative path from this module
|
|
|
|
|
is_zip: bool = False
|
2023-06-20 01:01:18 +02:00
|
|
|
relative: bool = True # relative to regular world import folder
|
2024-06-06 11:36:14 -07:00
|
|
|
time_taken: float = -1.0
|
2025-09-24 02:39:19 +02:00
|
|
|
version: Version = Version(0, 0, 0)
|
2022-08-15 23:52:03 +02:00
|
|
|
|
2023-11-04 10:26:51 +01:00
|
|
|
def __repr__(self) -> str:
|
2023-06-20 01:01:18 +02:00
|
|
|
return f"{self.__class__.__name__}({self.path}, is_zip={self.is_zip}, relative={self.relative})"
|
|
|
|
|
|
|
|
|
|
@property
|
|
|
|
|
def resolved_path(self) -> str:
|
|
|
|
|
if self.relative:
|
2023-11-04 10:26:51 +01:00
|
|
|
return os.path.join(local_folder, self.path)
|
2023-06-20 01:01:18 +02:00
|
|
|
return self.path
|
|
|
|
|
|
|
|
|
|
def load(self) -> bool:
|
|
|
|
|
try:
|
2024-02-04 09:09:07 +01:00
|
|
|
start = time.perf_counter()
|
2023-06-20 01:01:18 +02:00
|
|
|
if self.is_zip:
|
|
|
|
|
importer = zipimport.zipimporter(self.resolved_path)
|
2024-11-27 03:28:00 +01:00
|
|
|
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)
|
|
|
|
|
|
|
|
|
|
mod.__package__ = f"worlds.{mod.__package__}"
|
|
|
|
|
|
2023-06-20 01:01:18 +02:00
|
|
|
mod.__name__ = f"worlds.{mod.__name__}"
|
|
|
|
|
sys.modules[mod.__name__] = mod
|
|
|
|
|
with warnings.catch_warnings():
|
|
|
|
|
warnings.filterwarnings("ignore", message="__package__ != __spec__.parent")
|
2025-07-31 14:33:56 -06:00
|
|
|
importer.exec_module(mod)
|
2023-06-20 01:01:18 +02:00
|
|
|
else:
|
|
|
|
|
importlib.import_module(f".{self.path}", "worlds")
|
2024-02-04 09:09:07 +01:00
|
|
|
self.time_taken = time.perf_counter()-start
|
2023-06-20 01:01:18 +02:00
|
|
|
return True
|
|
|
|
|
|
2023-11-04 10:26:51 +01:00
|
|
|
except Exception:
|
2023-06-20 01:01:18 +02:00
|
|
|
# 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())
|
2024-03-28 22:21:56 +01:00
|
|
|
failed_world_loads.append(os.path.basename(self.path).rsplit(".", 1)[0])
|
2023-06-20 01:01:18 +02:00
|
|
|
return False
|
2023-03-20 21:24:47 +01:00
|
|
|
|
2022-08-15 23:52:03 +02:00
|
|
|
|
|
|
|
|
# find potential world containers, currently folders and zip-importable .apworld's
|
2023-11-18 12:29:35 -06:00
|
|
|
world_sources: List[WorldSource] = []
|
2023-11-04 10:26:51 +01:00
|
|
|
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():
|
2024-06-08 19:58:58 +02:00
|
|
|
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')):
|
2024-06-06 11:36:14 -07:00
|
|
|
world_sources.append(WorldSource(file_name, relative=relative))
|
|
|
|
|
else:
|
|
|
|
|
logging.warning(f"excluding {entry.name} from world sources because it has no __init__.py")
|
2023-11-04 10:26:51 +01:00
|
|
|
elif entry.is_file() and entry.name.endswith(".apworld"):
|
|
|
|
|
world_sources.append(WorldSource(file_name, is_zip=True, relative=relative))
|
2021-02-21 20:17:24 +01:00
|
|
|
|
2021-06-29 03:49:29 +02:00
|
|
|
# import all submodules to trigger AutoWorldRegister
|
2022-08-15 23:52:03 +02:00
|
|
|
world_sources.sort()
|
2025-09-24 02:39:19 +02:00
|
|
|
apworlds: list[WorldSource] = []
|
2022-08-15 23:52:03 +02:00
|
|
|
for world_source in world_sources:
|
2025-09-24 02:39:19 +02:00
|
|
|
# load all loose files first:
|
|
|
|
|
if world_source.is_zip:
|
|
|
|
|
apworlds.append(world_source)
|
|
|
|
|
else:
|
|
|
|
|
world_source.load()
|
2021-07-12 18:05:46 +02:00
|
|
|
|
2023-11-18 12:29:35 -06:00
|
|
|
from .AutoWorld import AutoWorldRegister
|
2021-07-12 18:05:46 +02:00
|
|
|
|
2025-09-24 02:39:19 +02:00
|
|
|
if apworlds:
|
|
|
|
|
# encapsulation for namespace / gc purposes
|
|
|
|
|
def load_apworlds() -> None:
|
|
|
|
|
global apworlds
|
|
|
|
|
from .Files import APWorldContainer, InvalidDataError
|
|
|
|
|
core_compatible: list[tuple[WorldSource, APWorldContainer]] = []
|
|
|
|
|
|
|
|
|
|
def fail_world(game_name: str, reason: str, add_as_failed_to_load: bool = True) -> None:
|
|
|
|
|
if add_as_failed_to_load:
|
|
|
|
|
failed_world_loads.append(game_name)
|
|
|
|
|
logging.warning(reason)
|
|
|
|
|
|
|
|
|
|
for apworld_source in apworlds:
|
|
|
|
|
apworld: APWorldContainer = APWorldContainer(apworld_source.resolved_path)
|
|
|
|
|
# populate metadata
|
|
|
|
|
try:
|
|
|
|
|
apworld.read()
|
|
|
|
|
except InvalidDataError as e:
|
|
|
|
|
if version_tuple < (0, 7, 0):
|
|
|
|
|
logging.error(
|
|
|
|
|
f"Invalid or missing manifest file for {apworld_source.resolved_path}. "
|
|
|
|
|
"This apworld will stop working with Archipelago 0.7.0."
|
|
|
|
|
)
|
|
|
|
|
logging.error(e)
|
|
|
|
|
else:
|
|
|
|
|
raise e
|
|
|
|
|
|
|
|
|
|
if apworld.minimum_ap_version and apworld.minimum_ap_version > version_tuple:
|
|
|
|
|
fail_world(apworld.game,
|
|
|
|
|
f"Did not load {apworld_source.path} "
|
|
|
|
|
f"as its minimum core version {apworld.minimum_ap_version} "
|
|
|
|
|
f"is higher than current core version {version_tuple}.")
|
|
|
|
|
elif apworld.maximum_ap_version and apworld.maximum_ap_version < version_tuple:
|
|
|
|
|
fail_world(apworld.game,
|
|
|
|
|
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}.")
|
|
|
|
|
else:
|
|
|
|
|
core_compatible.append((apworld_source, apworld))
|
|
|
|
|
# load highest version first
|
|
|
|
|
core_compatible.sort(
|
|
|
|
|
key=lambda element: element[1].world_version if element[1].world_version else Version(0, 0, 0),
|
|
|
|
|
reverse=True)
|
|
|
|
|
for apworld_source, apworld in core_compatible:
|
|
|
|
|
if apworld.game and apworld.game in AutoWorldRegister.world_types:
|
|
|
|
|
fail_world(apworld.game,
|
|
|
|
|
f"Did not load {apworld_source.path} "
|
|
|
|
|
f"as its game {apworld.game} is already loaded.",
|
|
|
|
|
add_as_failed_to_load=False)
|
|
|
|
|
else:
|
|
|
|
|
apworld_source.load()
|
|
|
|
|
load_apworlds()
|
|
|
|
|
del load_apworlds
|
|
|
|
|
|
|
|
|
|
del apworlds
|
|
|
|
|
|
|
|
|
|
# Build the data package for each game.
|
2022-11-28 02:25:53 +01:00
|
|
|
network_data_package: DataPackage = {
|
2023-11-18 12:29:35 -06:00
|
|
|
"games": {world_name: world.get_data_package_data() for world_name, world in AutoWorldRegister.world_types.items()},
|
2021-07-12 18:05:46 +02:00
|
|
|
}
|
2024-06-14 15:53:42 -07:00
|
|
|
|