2020-04-22 03:09:46 +00:00
|
|
|
from __future__ import annotations
|
2021-01-02 11:49:43 +00:00
|
|
|
|
2022-11-02 14:51:35 +00:00
|
|
|
import asyncio
|
2023-03-20 16:01:08 +00:00
|
|
|
import json
|
2020-06-21 13:32:31 +00:00
|
|
|
import typing
|
2021-11-28 03:06:30 +00:00
|
|
|
import builtins
|
|
|
|
import os
|
|
|
|
import subprocess
|
|
|
|
import sys
|
|
|
|
import pickle
|
|
|
|
import functools
|
|
|
|
import io
|
|
|
|
import collections
|
|
|
|
import importlib
|
|
|
|
import logging
|
2023-02-17 18:16:37 +00:00
|
|
|
from typing import BinaryIO, Coroutine, Optional, Set, Dict, Any, Union
|
2022-09-29 22:36:30 +00:00
|
|
|
|
2022-08-11 22:32:37 +00:00
|
|
|
from yaml import load, load_all, dump, SafeLoader
|
|
|
|
|
|
|
|
try:
|
|
|
|
from yaml import CLoader as UnsafeLoader
|
|
|
|
from yaml import CDumper as Dumper
|
|
|
|
except ImportError:
|
|
|
|
from yaml import Loader as UnsafeLoader
|
|
|
|
from yaml import Dumper
|
2022-05-18 20:30:19 +00:00
|
|
|
|
|
|
|
if typing.TYPE_CHECKING:
|
2022-08-11 22:32:37 +00:00
|
|
|
import tkinter
|
|
|
|
import pathlib
|
2020-06-21 13:32:31 +00:00
|
|
|
|
|
|
|
|
2021-08-06 17:33:17 +00:00
|
|
|
def tuplize_version(version: str) -> Version:
|
2020-12-29 18:23:14 +00:00
|
|
|
return Version(*(int(piece, 10) for piece in version.split(".")))
|
|
|
|
|
2020-06-21 13:32:31 +00:00
|
|
|
|
2020-12-29 18:23:14 +00:00
|
|
|
class Version(typing.NamedTuple):
|
|
|
|
major: int
|
|
|
|
minor: int
|
2021-02-21 22:46:05 +00:00
|
|
|
build: int
|
2020-04-22 03:09:46 +00:00
|
|
|
|
2023-04-25 11:26:52 +00:00
|
|
|
def as_simple_string(self) -> str:
|
|
|
|
return ".".join(str(item) for item in self)
|
|
|
|
|
2021-07-01 23:29:49 +00:00
|
|
|
|
2023-06-29 13:06:58 +00:00
|
|
|
__version__ = "0.4.2"
|
2021-06-18 20:15:54 +00:00
|
|
|
version_tuple = tuplize_version(__version__)
|
2020-04-20 12:50:49 +00:00
|
|
|
|
2022-08-11 22:32:37 +00:00
|
|
|
is_linux = sys.platform.startswith("linux")
|
|
|
|
is_macos = sys.platform == "darwin"
|
2022-06-04 16:10:34 +00:00
|
|
|
is_windows = sys.platform in ("win32", "cygwin", "msys")
|
|
|
|
|
2017-11-28 14:36:32 +00:00
|
|
|
|
2022-04-28 16:03:44 +00:00
|
|
|
def int16_as_bytes(value: int) -> typing.List[int]:
|
2018-02-17 23:38:54 +00:00
|
|
|
value = value & 0xFFFF
|
|
|
|
return [value & 0xFF, (value >> 8) & 0xFF]
|
|
|
|
|
2020-02-16 14:32:40 +00:00
|
|
|
|
2022-04-28 16:03:44 +00:00
|
|
|
def int32_as_bytes(value: int) -> typing.List[int]:
|
2018-02-17 23:38:54 +00:00
|
|
|
value = value & 0xFFFFFFFF
|
|
|
|
return [value & 0xFF, (value >> 8) & 0xFF, (value >> 16) & 0xFF, (value >> 24) & 0xFF]
|
|
|
|
|
2020-02-16 14:32:40 +00:00
|
|
|
|
2022-04-28 16:03:44 +00:00
|
|
|
def pc_to_snes(value: int) -> int:
|
2021-01-02 11:49:43 +00:00
|
|
|
return ((value << 1) & 0x7F0000) | (value & 0x7FFF) | 0x8000
|
2018-09-23 02:51:54 +00:00
|
|
|
|
2020-07-21 21:15:19 +00:00
|
|
|
|
2022-04-28 16:03:44 +00:00
|
|
|
def snes_to_pc(value: int) -> int:
|
2021-01-02 11:49:43 +00:00
|
|
|
return ((value & 0x7F0000) >> 1) | (value & 0x7FFF)
|
2018-09-23 02:51:54 +00:00
|
|
|
|
2020-07-21 21:15:19 +00:00
|
|
|
|
2022-04-28 16:03:44 +00:00
|
|
|
RetType = typing.TypeVar("RetType")
|
|
|
|
|
|
|
|
|
|
|
|
def cache_argsless(function: typing.Callable[[], RetType]) -> typing.Callable[[], RetType]:
|
2022-04-30 02:39:08 +00:00
|
|
|
assert not function.__code__.co_argcount, "Can only cache 0 argument functions with this cache."
|
2021-07-09 15:44:24 +00:00
|
|
|
|
2022-04-28 16:03:44 +00:00
|
|
|
sentinel = object()
|
|
|
|
result: typing.Union[object, RetType] = sentinel
|
2021-07-09 15:44:24 +00:00
|
|
|
|
2022-04-28 16:03:44 +00:00
|
|
|
def _wrap() -> RetType:
|
2021-07-09 15:44:24 +00:00
|
|
|
nonlocal result
|
|
|
|
if result is sentinel:
|
|
|
|
result = function()
|
2022-04-28 16:03:44 +00:00
|
|
|
return typing.cast(RetType, result)
|
2021-07-09 15:44:24 +00:00
|
|
|
|
|
|
|
return _wrap
|
|
|
|
|
|
|
|
|
2021-07-19 19:52:08 +00:00
|
|
|
def is_frozen() -> bool:
|
2022-04-28 16:03:44 +00:00
|
|
|
return typing.cast(bool, getattr(sys, 'frozen', False))
|
2017-11-28 14:36:32 +00:00
|
|
|
|
2020-07-21 21:15:19 +00:00
|
|
|
|
2022-03-31 03:08:15 +00:00
|
|
|
def local_path(*path: str) -> str:
|
2023-03-29 18:14:45 +00:00
|
|
|
"""
|
|
|
|
Returns path to a file in the local Archipelago installation or source.
|
|
|
|
This might be read-only and user_path should be used instead for ROMs, configuration, etc.
|
|
|
|
"""
|
2022-03-31 03:08:15 +00:00
|
|
|
if hasattr(local_path, 'cached_path'):
|
|
|
|
pass
|
2021-07-19 19:52:08 +00:00
|
|
|
elif is_frozen():
|
2020-03-23 06:45:40 +00:00
|
|
|
if hasattr(sys, "_MEIPASS"):
|
|
|
|
# we are running in a PyInstaller bundle
|
|
|
|
local_path.cached_path = sys._MEIPASS # pylint: disable=protected-access,no-member
|
|
|
|
else:
|
|
|
|
# cx_Freeze
|
|
|
|
local_path.cached_path = os.path.dirname(os.path.abspath(sys.argv[0]))
|
2017-11-28 14:36:32 +00:00
|
|
|
else:
|
2020-03-23 06:45:40 +00:00
|
|
|
import __main__
|
2022-12-11 12:15:23 +00:00
|
|
|
if hasattr(__main__, "__file__") and os.path.isfile(__main__.__file__):
|
2021-04-04 01:18:19 +00:00
|
|
|
# we are running in a normal Python environment
|
|
|
|
local_path.cached_path = os.path.dirname(os.path.abspath(__main__.__file__))
|
|
|
|
else:
|
|
|
|
# pray
|
|
|
|
local_path.cached_path = os.path.abspath(".")
|
2020-03-15 18:32:00 +00:00
|
|
|
|
2020-08-25 11:22:47 +00:00
|
|
|
return os.path.join(local_path.cached_path, *path)
|
2017-11-28 14:36:32 +00:00
|
|
|
|
2021-01-02 11:49:43 +00:00
|
|
|
|
2022-03-31 03:08:15 +00:00
|
|
|
def home_path(*path: str) -> str:
|
|
|
|
"""Returns path to a file in the user home's Archipelago directory."""
|
|
|
|
if hasattr(home_path, 'cached_path'):
|
|
|
|
pass
|
|
|
|
elif sys.platform.startswith('linux'):
|
|
|
|
home_path.cached_path = os.path.expanduser('~/Archipelago')
|
|
|
|
os.makedirs(home_path.cached_path, 0o700, exist_ok=True)
|
|
|
|
else:
|
|
|
|
# not implemented
|
|
|
|
home_path.cached_path = local_path() # this will generate the same exceptions we got previously
|
|
|
|
|
|
|
|
return os.path.join(home_path.cached_path, *path)
|
2017-11-28 14:36:32 +00:00
|
|
|
|
2020-08-25 11:22:47 +00:00
|
|
|
|
2022-03-31 03:08:15 +00:00
|
|
|
def user_path(*path: str) -> str:
|
|
|
|
"""Returns either local_path or home_path based on write permissions."""
|
2022-08-11 22:32:37 +00:00
|
|
|
if hasattr(user_path, "cached_path"):
|
2022-03-31 03:08:15 +00:00
|
|
|
pass
|
|
|
|
elif os.access(local_path(), os.W_OK):
|
|
|
|
user_path.cached_path = local_path()
|
|
|
|
else:
|
|
|
|
user_path.cached_path = home_path()
|
|
|
|
# populate home from local - TODO: upgrade feature
|
2022-08-11 22:32:37 +00:00
|
|
|
if user_path.cached_path != local_path() and not os.path.exists(user_path("host.yaml")):
|
|
|
|
import shutil
|
|
|
|
for dn in ("Players", "data/sprites"):
|
2022-03-31 03:08:15 +00:00
|
|
|
shutil.copytree(local_path(dn), user_path(dn), dirs_exist_ok=True)
|
2022-08-11 22:32:37 +00:00
|
|
|
for fn in ("manifest.json", "host.yaml"):
|
2022-03-31 03:08:15 +00:00
|
|
|
shutil.copy2(local_path(fn), user_path(fn))
|
|
|
|
|
|
|
|
return os.path.join(user_path.cached_path, *path)
|
|
|
|
|
|
|
|
|
2023-03-20 16:01:08 +00:00
|
|
|
def cache_path(*path: str) -> str:
|
|
|
|
"""Returns path to a file in the user's Archipelago cache directory."""
|
|
|
|
if hasattr(cache_path, "cached_path"):
|
|
|
|
pass
|
|
|
|
else:
|
2023-03-30 13:30:43 +00:00
|
|
|
import platformdirs
|
|
|
|
cache_path.cached_path = platformdirs.user_cache_dir("Archipelago", False)
|
2023-03-20 16:01:08 +00:00
|
|
|
|
|
|
|
return os.path.join(cache_path.cached_path, *path)
|
|
|
|
|
|
|
|
|
2022-10-25 17:54:43 +00:00
|
|
|
def output_path(*path: str) -> str:
|
2022-03-31 03:08:15 +00:00
|
|
|
if hasattr(output_path, 'cached_path'):
|
2020-08-25 11:22:47 +00:00
|
|
|
return os.path.join(output_path.cached_path, *path)
|
2022-03-31 03:08:15 +00:00
|
|
|
output_path.cached_path = user_path(get_options()["general_options"]["output_path"])
|
2020-08-25 11:22:47 +00:00
|
|
|
path = os.path.join(output_path.cached_path, *path)
|
2020-08-01 14:52:11 +00:00
|
|
|
os.makedirs(os.path.dirname(path), exist_ok=True)
|
|
|
|
return path
|
2017-11-28 14:36:32 +00:00
|
|
|
|
2021-01-02 11:49:43 +00:00
|
|
|
|
2022-08-11 22:32:37 +00:00
|
|
|
def open_file(filename: typing.Union[str, "pathlib.Path"]) -> None:
|
|
|
|
if is_windows:
|
2017-11-28 14:36:32 +00:00
|
|
|
os.startfile(filename)
|
|
|
|
else:
|
2022-08-11 22:32:37 +00:00
|
|
|
from shutil import which
|
|
|
|
open_command = which("open") if is_macos else (which("xdg-open") or which("gnome-open") or which("kde-open"))
|
2017-11-28 14:36:32 +00:00
|
|
|
subprocess.call([open_command, filename])
|
2017-12-02 14:21:04 +00:00
|
|
|
|
2021-01-02 11:49:43 +00:00
|
|
|
|
2022-01-19 03:26:25 +00:00
|
|
|
# from https://gist.github.com/pypt/94d747fe5180851196eb#gistcomment-4015118 with some changes
|
|
|
|
class UniqueKeyLoader(SafeLoader):
|
|
|
|
def construct_mapping(self, node, deep=False):
|
|
|
|
mapping = set()
|
|
|
|
for key_node, value_node in node.value:
|
|
|
|
key = self.construct_object(key_node, deep=deep)
|
|
|
|
if key in mapping:
|
2022-01-25 03:20:08 +00:00
|
|
|
logging.error(f"YAML duplicates sanity check failed{key_node.start_mark}")
|
|
|
|
raise KeyError(f"Duplicate key {key} found in YAML. Already found keys: {mapping}.")
|
2022-01-19 03:26:25 +00:00
|
|
|
mapping.add(key)
|
|
|
|
return super().construct_mapping(node, deep)
|
|
|
|
|
|
|
|
|
|
|
|
parse_yaml = functools.partial(load, Loader=UniqueKeyLoader)
|
2022-04-12 08:57:29 +00:00
|
|
|
parse_yamls = functools.partial(load_all, Loader=UniqueKeyLoader)
|
2022-08-11 22:32:37 +00:00
|
|
|
unsafe_parse_yaml = functools.partial(load, Loader=UnsafeLoader)
|
|
|
|
|
|
|
|
del load, load_all # should not be used. don't leak their names
|
2020-02-16 14:32:40 +00:00
|
|
|
|
2021-07-30 23:40:27 +00:00
|
|
|
|
2021-11-13 22:14:26 +00:00
|
|
|
def get_cert_none_ssl_context():
|
|
|
|
import ssl
|
|
|
|
ctx = ssl.create_default_context()
|
|
|
|
ctx.check_hostname = False
|
|
|
|
ctx.verify_mode = ssl.CERT_NONE
|
|
|
|
return ctx
|
|
|
|
|
|
|
|
|
2021-07-09 15:44:24 +00:00
|
|
|
@cache_argsless
|
2020-03-05 23:48:23 +00:00
|
|
|
def get_public_ipv4() -> str:
|
|
|
|
import socket
|
|
|
|
import urllib.request
|
|
|
|
ip = socket.gethostbyname(socket.gethostname())
|
2021-11-13 22:14:26 +00:00
|
|
|
ctx = get_cert_none_ssl_context()
|
2020-03-05 23:48:23 +00:00
|
|
|
try:
|
2023-03-09 20:31:00 +00:00
|
|
|
ip = urllib.request.urlopen("https://checkip.amazonaws.com/", context=ctx, timeout=10).read().decode("utf8").strip()
|
2020-03-05 23:48:23 +00:00
|
|
|
except Exception as e:
|
2022-08-11 22:32:37 +00:00
|
|
|
# noinspection PyBroadException
|
2020-03-05 23:48:23 +00:00
|
|
|
try:
|
2023-03-09 20:31:00 +00:00
|
|
|
ip = urllib.request.urlopen("https://v4.ident.me", context=ctx, timeout=10).read().decode("utf8").strip()
|
2022-08-11 22:32:37 +00:00
|
|
|
except Exception:
|
2020-03-05 23:48:23 +00:00
|
|
|
logging.exception(e)
|
|
|
|
pass # we could be offline, in a local game, so no point in erroring out
|
|
|
|
return ip
|
2020-03-15 18:32:00 +00:00
|
|
|
|
2021-07-30 23:40:27 +00:00
|
|
|
|
2021-07-09 15:44:24 +00:00
|
|
|
@cache_argsless
|
2020-06-14 07:06:37 +00:00
|
|
|
def get_public_ipv6() -> str:
|
|
|
|
import socket
|
|
|
|
import urllib.request
|
|
|
|
ip = socket.gethostbyname(socket.gethostname())
|
2021-11-13 22:14:26 +00:00
|
|
|
ctx = get_cert_none_ssl_context()
|
2020-06-14 07:06:37 +00:00
|
|
|
try:
|
2023-03-09 20:31:00 +00:00
|
|
|
ip = urllib.request.urlopen("https://v6.ident.me", context=ctx, timeout=10).read().decode("utf8").strip()
|
2020-06-14 07:06:37 +00:00
|
|
|
except Exception as e:
|
|
|
|
logging.exception(e)
|
2020-06-21 14:13:42 +00:00
|
|
|
pass # we could be offline, in a local game, or ipv6 may not be available
|
2020-06-14 07:06:37 +00:00
|
|
|
return ip
|
2020-03-15 18:32:00 +00:00
|
|
|
|
2021-07-30 23:40:27 +00:00
|
|
|
|
2022-09-28 21:54:10 +00:00
|
|
|
OptionsType = typing.Dict[str, typing.Dict[str, typing.Any]]
|
|
|
|
|
|
|
|
|
2021-07-09 15:44:24 +00:00
|
|
|
@cache_argsless
|
2022-09-28 21:54:10 +00:00
|
|
|
def get_default_options() -> OptionsType:
|
2021-07-09 15:44:24 +00:00
|
|
|
# Refer to host.yaml for comments as to what all these options mean.
|
|
|
|
options = {
|
|
|
|
"general_options": {
|
|
|
|
"output_path": "output",
|
|
|
|
},
|
|
|
|
"factorio_options": {
|
2022-02-18 17:54:26 +00:00
|
|
|
"executable": os.path.join("factorio", "bin", "x64", "factorio"),
|
2022-10-27 22:07:57 +00:00
|
|
|
"filter_item_sends": False,
|
2022-10-27 22:45:26 +00:00
|
|
|
"bridge_chat_out": True,
|
2021-07-09 15:44:24 +00:00
|
|
|
},
|
2022-10-25 17:54:43 +00:00
|
|
|
"sni_options": {
|
2022-11-20 10:14:13 +00:00
|
|
|
"sni_path": "SNI",
|
2022-10-25 17:54:43 +00:00
|
|
|
"snes_rom_start": True,
|
|
|
|
},
|
2021-11-12 13:00:11 +00:00
|
|
|
"sm_options": {
|
2021-11-12 13:36:34 +00:00
|
|
|
"rom_file": "Super Metroid (JU).sfc",
|
2021-11-12 13:00:11 +00:00
|
|
|
},
|
2021-11-13 19:52:30 +00:00
|
|
|
"soe_options": {
|
|
|
|
"rom_file": "Secret of Evermore (USA).sfc",
|
|
|
|
},
|
2021-07-09 15:44:24 +00:00
|
|
|
"lttp_options": {
|
|
|
|
"rom_file": "Zelda no Densetsu - Kamigami no Triforce (Japan).sfc",
|
|
|
|
},
|
2023-03-20 16:26:03 +00:00
|
|
|
"ladx_options": {
|
|
|
|
"rom_file": "Legend of Zelda, The - Link's Awakening DX (USA, Europe) (SGB Enhanced).gbc",
|
|
|
|
},
|
2021-07-09 15:44:24 +00:00
|
|
|
"server_options": {
|
|
|
|
"host": None,
|
|
|
|
"port": 38281,
|
|
|
|
"password": None,
|
|
|
|
"multidata": None,
|
|
|
|
"savefile": None,
|
|
|
|
"disable_save": False,
|
|
|
|
"loglevel": "info",
|
|
|
|
"server_password": None,
|
|
|
|
"disable_item_cheat": False,
|
|
|
|
"location_check_points": 1,
|
|
|
|
"hint_cost": 10,
|
2023-01-02 18:48:31 +00:00
|
|
|
"release_mode": "goal",
|
2021-10-18 20:58:29 +00:00
|
|
|
"collect_mode": "disabled",
|
2021-07-09 15:44:24 +00:00
|
|
|
"remaining_mode": "goal",
|
|
|
|
"auto_shutdown": 0,
|
|
|
|
"compatibility": 2,
|
|
|
|
"log_network": 0
|
|
|
|
},
|
2021-07-21 16:08:15 +00:00
|
|
|
"generator": {
|
2022-06-01 23:49:57 +00:00
|
|
|
"enemizer_path": os.path.join("EnemizerCLI", "EnemizerCLI.Core"),
|
2021-07-09 15:44:24 +00:00
|
|
|
"player_files_path": "Players",
|
|
|
|
"players": 0,
|
|
|
|
"weights_file_path": "weights.yaml",
|
|
|
|
"meta_file_path": "meta.yaml",
|
2022-12-11 19:48:26 +00:00
|
|
|
"spoiler": 3,
|
2021-07-09 15:44:24 +00:00
|
|
|
"glitch_triforce_room": 1,
|
|
|
|
"race": 0,
|
|
|
|
"plando_options": "bosses",
|
2021-08-15 00:32:36 +00:00
|
|
|
},
|
|
|
|
"minecraft_options": {
|
|
|
|
"forge_directory": "Minecraft Forge server",
|
2022-05-11 04:00:53 +00:00
|
|
|
"max_heap_size": "2G",
|
|
|
|
"release_channel": "release"
|
2021-08-15 00:32:36 +00:00
|
|
|
},
|
Ocarina of Time (#64)
* first commit (not including OoT data files yet)
* added some basic options
* rule parser works now at least
* make sure to commit everything this time
* temporary change to BaseClasses for oot
* overworld location graph builds mostly correctly
* adding oot data files
* commenting out world options until later since they only existed to make the RuleParser work
* conversion functions between AP ids and OOT ids
* world graph outputs
* set scrub prices
* itempool generates, entrances connected, way too many options added
* fixed set_rules and set_shop_rules
* temp baseclasses changes
* Reaches the fill step now, old event-based system retained in case the new way breaks
* Song placements and misc fixes everywhere
* temporary changes to make oot work
* changed root exits for AP fill framework
* prevent infinite recursion due to OoT sharing usage of the address field
* age reachability works hopefully, songs are broken again
* working spoiler log generation on beatable-only
* Logic tricks implemented
* need this for logic tricks
* fixed map/compass being placed on Serenade location
* kill unreachable events before filling the world
* add a bunch of utility functions to prepare for rom patching
* move OptionList into generic options
* fixed some silly bugs with OptionList
* properly seed all random behavior (so far)
* ROM generation working
* fix hints trying to get alttp dungeon hint texts
* continue fixing hints
* add oot to network data package
* change item and location IDs to 66000 and 67000 range respectively
* push removed items to precollected items
* fixed various issues with cross-contamination with multiple world generation
* reenable glitched logic (hopefully)
* glitched world files age-check fix
* cleaned up some get_locations calls
* added token shuffle and scrub shuffle, modified some options slightly to make the parsing work
* reenable MQ dungeons
* fix forest mq exception
* made targeting style an option for now, will be cosmetic later
* reminder to move targeting to cosmetics
* some oot option maintenance
* enabled starting time of day
* fixed issue breaking shop slots in multiworld generation
* added "off" option for text shuffle and hints
* shopsanity functionality restored
* change patch file extension
* remove unnecessary utility functions + imports
* update MIT license
* change option to "patch_uncompressed_rom" instead of "compress_rom"
* compliance with new AutoWorld systems
* Kill only internal events, remove non-internal big poe event in code
* re-add the big poe event and handle it correctly
* remove extra method in Range option
* fix typo
* Starting items, starting with consumables option
* do not remove nonexistent item
* move set_shop_rules to after shop items are placed
* some cleanup
* add retries for song placement
* flagged Skull Mask and Mask of Truth as advancement items
* update OoT to use LogicMixin
* Fixed trying to assign starting items from the wrong players
* fixed song retry step
* improved option handling, comments, and starting item replacements
* DefaultOnToggle writes Yes or No to spoiler
* enable compression of output if Compress executable is present
* clean up compression
* check whether (de)compressor exists before running the process
* allow specification of rom path in host.yaml
* check if decompressed file already exists before decompressing again
* fix triforce hunt generation
* rename all the oot state functions with prefix
* OoT: mark triforce pieces as completion goal for triforce hunt
* added overworld and any-dungeon shuffle for dungeon items
* Hide most unshuffled locations and events from the list of locations in spoiler
* build oot option ranges with a generic function instead of defining each separately
* move oot output-type control to host.yaml instead of individual yamls
* implement dungeon song shuffle
* minor improvements to overworld dungeon item shuffle
* remove random ice trap names in shops, mostly to avoid maintaining a massive censor list
* always output patch file to folder, remove option to generate ROM in preparation for removal
* re-add the fix for infinite recursion due to not being light or dark world
* change AP-sendable to Ocarina of Time model, since the triforce piece has some extra code apparently
* oot: remove item_names and location_names
* oot: minor fixes
* oot: comment out ROM patching
* oot: only add CollectionState objects on creation if actually needed
* main entrance shuffle method and entrances-based rules
* fix entrances based rules
* disable master quest and big poe count options for client compatibility
* use get_player_name instead of get_player_names
* fix OptionList
* fix oot options for new option system
* new coop section in oot rom: expand player names to 16 bytes, write AP_PLAYER_NAME at end of PLAYER_NAMES
* fill AP player name in oot rom with 0 instead of 0xDF
* encode player name with ASCII for fixed-width
* revert oot player name array to 8 bytes per name
* remove Pierre location if fast scarecrow is on
* check player name length
* "free_scarecrow" not "fast_scarecrow"
* OoT locations now properly store the AP ID instead of the oot internal ID
* oot __version__ updates in lockstep with AP version
* pull in unmodified oot cosmetic files
* also grab JSONDump since it's needed apparently
* gather extra needed methods, modify imports
* delete cosmetics log, replace all instances of SettingsList with OOTWorld
* cosmetic options working, except for sound effects (due to ear-safe issues)
* SFX, Music, and Fanfare randomization reenabled
* move OoT data files into the worlds folder
* move Compress and Decompress into oot data folder
* Replace get_all_state with custom method to avoid the cache
* OoT ROM: increment item counter before setting incoming item/player values to 0, preventing desync issues
* set data_version to 0
* make Kokiri Sword shuffle off by default
* reenable "Random Choice" for various cosmetic options
* kill Ruto's Letter turnin if open fountain
also fix for shopsanity
* place Buy Goron/Zora Tunic first in shop shuffle
* make ice traps appear as other items instead of breaking generation
* managed to break ice traps on non-major-only
* only handle ice traps if they are on
* fix shopsanity for non-oot games, and write player name instead of player number
* light arrows hint uses player name instead of player number
* Reenable "skip child zelda" option
* fix entrances_based_rules
* fix ganondorf hint if starting with light arrows
* fix dungeonitem shuffle and shopsanity interaction
* remove has_all_of, has_any_of, count_of in BaseClasses, replace usage with has_all, has_any, has_group
* force local giveable item on ZL if skip_child_zelda and shuffle_song_items is any
* keep bosses and bombchu bowling chus out of data package
* revert workaround for infinite recursion and fix it properly
* fix shared shop id caches during patching process
* fix shop text box overflows, as much as possible
* add default oot host.yaml option
* add .apz5, .n64, .z64 to gitignore
* Properly document and name all (functioning) OOT options
* clean up some imports
* remove unnecessary files from oot's data
* fix typo in gitignore
* readd the Compress and Decompress utilities, since they are needed for generation
* cleanup of imports and some minor optimizations
* increase shop offset for item IDs to 0xCB
* remove shop item AP ids entirely
* prevent triforce pieces for other players from being received by yourself
* add "excluded" property to Location
* Hint system adapted and reenabled; hints still unseeded
* make hints deterministic with lists instead of sets
* do not allow hints to point to Light Arrows on non-vanilla bridge
* foreign locations hint as their full name in OoT rather than their region
* checkedLocations now stores hint names by player ID, so that the same location in different worlds can have hints associated
* consolidate versioning in Utils
* ice traps appear as major items rather than any progression item
* set prescription and claim check as defaults for adult trade item settings
* add oot options to playerSettings
* allow case-insensitive logic tricks in yaml
* fix oot shopsanity option formatting
* Write OoT override info even if local item, enabling local checks to show up immediately in the client
* implement CollectionState.can_live_dmg for oot glitched logic
* filter item names for invalid characters when patching shops
* make ice traps appear according to the settings of the world they are shuffled into, rather than the original world
* set hidden-spoiler items and locations with Shop items to events
* make GF carpenters, Gerudo Card, Malon, ZL, and Impa events if the relevant settings are enabled, preventing them from appearing in the client on game start
* Fix oot Glitched and No Logic generation
* fix indenting
* Greatly reduce displayed cosmetic options
* Change oot data version to 1
* add apz5 distribution to webhost
* print player name if an ALttP dungeon contains a good item for OoT world
* delete unneeded commented code
* remove OcarinaSongs import to satisfy lint
2021-09-02 12:35:05 +00:00
|
|
|
"oot_options": {
|
|
|
|
"rom_file": "The Legend of Zelda - Ocarina of Time.z64",
|
2022-12-08 01:06:34 +00:00
|
|
|
"rom_start": True
|
2022-07-26 17:25:49 +00:00
|
|
|
},
|
|
|
|
"dkc3_options": {
|
|
|
|
"rom_file": "Donkey Kong Country 3 - Dixie Kong's Double Trouble! (USA) (En,Fr).sfc",
|
|
|
|
},
|
2022-09-29 18:16:59 +00:00
|
|
|
"smw_options": {
|
|
|
|
"rom_file": "Super Mario World (USA).sfc",
|
|
|
|
},
|
2022-10-20 17:41:11 +00:00
|
|
|
"zillion_options": {
|
|
|
|
"rom_file": "Zillion (UE) [!].sms",
|
|
|
|
# RetroArch doesn't make it easy to launch a game from the command line.
|
|
|
|
# You have to know the path to the emulator core library on the user's computer.
|
|
|
|
"rom_start": "retroarch",
|
|
|
|
},
|
2022-10-13 05:45:52 +00:00
|
|
|
"pokemon_rb_options": {
|
|
|
|
"red_rom_file": "Pokemon Red (UE) [S][!].gb",
|
|
|
|
"blue_rom_file": "Pokemon Blue (UE) [S][!].gb",
|
|
|
|
"rom_start": True
|
2022-12-08 01:06:34 +00:00
|
|
|
},
|
|
|
|
"ffr_options": {
|
|
|
|
"display_msgs": True,
|
|
|
|
},
|
2022-12-12 01:36:18 +00:00
|
|
|
"lufia2ac_options": {
|
|
|
|
"rom_file": "Lufia II - Rise of the Sinistrals (USA).sfc",
|
|
|
|
},
|
2023-03-05 12:31:31 +00:00
|
|
|
"tloz_options": {
|
|
|
|
"rom_file": "Legend of Zelda, The (U) (PRG0) [!].nes",
|
|
|
|
"rom_start": True,
|
|
|
|
"display_msgs": True,
|
|
|
|
},
|
2023-02-24 06:35:09 +00:00
|
|
|
"wargroove_options": {
|
|
|
|
"root_directory": "C:/Program Files (x86)/Steam/steamapps/common/Wargroove"
|
2023-03-22 14:25:55 +00:00
|
|
|
},
|
Mega Man Battle Network 3: Implement New Game (#1198)
* Initializes MMBN3 world with empty files
* Adds item names to item dict
* Adds locations and names
* Adds skeleton of MMBN3Client. Mostly copy pasta from OOT
* Fixed some style and formatting
* More incremental Lua tests
* Adds all locations and checking to Lua connector
* Made class definitions for TextPet Parser
* Begun connecting item delivery system through lua and textpet
* Lua Connection can now send test items
* Item Delivery is now parameterized. Test command can send any chip
* Adds the ability to send non-chip items
* Fixes name errors in python client
* Fixes count for zenny, attempts to fix bugfrags
* Fixes an issue where you always received 255 bugfrags
* Converts zenny and bugfrag amounts to little endian bytecode
* Checks game state before sending chips
Adds debug option to display information overlayed on rom
Fixes chip indexing issue for chips with ids over 255
Minor text fixes
* Adds in some animation reset instructions during item get message
* Stores previously collected item index in save, re-sends missing items
* Adds title screen check before sending locations
Loading items from save could not be done via RAM. Had to be added in
assembly
* Adds progressive undernet check
* Added library for lzss decoding bits of rom
* More progress on parsing text events from ROM
* Adds a way to inject messages into ScriptArchive data structure and generate bytecode
* Adds Item definitions, passes to client
* Adds regions and item collection rules
* Touched up a few names and values that have changed in preparation for the final patching
* Modifying messages via item is now successful
* Added generate_output hook to generate ROM data
* Generates ROM successfully
* Fixes navi cust give index
* Whoops forgot to wrap this in brackets
* Injects extra scripts for undernet rankings
* Programs had ammount and color swapped
* Prompts the user for their username when connecting
* Adds flagClear to the list of commands to avoid overwriting
* Fixes message box crashes and several other multiworld issues
* Fixes IDs and names of several items and locations
* Added .gba to gitignore
* Fixes compatibility after recent rebase
* Fixes some locations and items that are otherwise unobtainable
* Attempts to make a working launcher in the installer
* Creates installer and fixes several inaccessible locations
* Many minor changes to items, locations, and requirements made during testing
* Adds an info page for MMBN3
* Fixes failing tests by removing duplicate IDs and properly marking progression items
* Accidentally forgot to un-remove the thing
* Whoops, changed this by accident
* Updates self.world references to self.multiworld
* Fixes imports to use from imports instead of using the namespace
* Removed some leftover merge artifacts from inno setup
* Puts back that darned signtool line again
* Adds Overworld Metro keys as items
* Adds TamaCode and puts shortcuts behind cyber passes
* Fixes Numberman code 16 check
* Fixes metro access logic and adds text to metro
* Reworks Lua to fix crashing when many items are queued
* Items for other BN3 games for different players are no longer given in the main player's ROM as well
* Fixes incorrect Item ID for ACDC Metro
* Fixes multi-box text messages
* Adds timer before sending an item
* Forgot to remove the second box of SubMems
* Updates patch and lua to prevent softlocks and crashes
* Adds options for extra undernet ranks, exclude jobs
* Extra GigFreez now gives 20 bugfrags
* Additional Progressive Undernets can no longer appear on the WWW Base
* Moves item signal byte to empty area of flags instead of end of RAM
* Adds Chocolate Shop locations and navi chips to fill them
* Fixes save crash, and added chocolates to lua
* Fixes chocolate stand selling out text, removes DrillMan cube in Undernet
* Replaces old messaging system with direct memory manipulation for receiving items
* Removes NDSPY requirements from MMBN3 by manually adapting the GBA's lz10 algorithm
* Fixes the names of Hospital-1 Locations
* Adds Canary Bit to avoid sending checks when title screen check fails
* Gaining a cybermetro pass will now open the shortcut immediately
* Randomizes the two accessible areas of Undernet 7, adds Hammer as item
* Adds new locations to connector lua
* Injects the name of the item into trade quests
* Fixes copy-paste error in docs
* Fixes merge artifacts and depracated code
* Nut-wafer stand now faces Lan the right way after buying
* Removes unused Goal Option and updates the readme to include most recent changes
* Touch-ups and formatting changes
* The Great Fillerization update. Dozens of items changed to Filler
* Replaces instances of Mega Man with MegaMan
* Update worlds/mmbn3/docs/en_MegaMan Battle Network 3.md
Co-authored-by: el-u <109771707+el-u@users.noreply.github.com>
* Update worlds/mmbn3/__init__.py
Co-authored-by: el-u <109771707+el-u@users.noreply.github.com>
* Apply suggestions from code review
Co-authored-by: el-u <109771707+el-u@users.noreply.github.com>
Co-authored-by: SoldierofOrder <107806872+SoldierofOrder@users.noreply.github.com>
* Changes code ordering to suit base class's
* assert_generate now checks for roms. Minor text fixes
* Makes player specific frequency and excluded location options
* Apply suggestions from code review
Co-authored-by: el-u <109771707+el-u@users.noreply.github.com>
* Addresses suggested changes from PR review
* Replaces ndspy lz10 with MIT-compliant nlzss lz10
* apworld compatibility fix for mmbn3_options from utils
* Addressing more comments by el-u
* APworld will now pull patch from zip folder
* Apply suggestions from code review
Co-authored-by: el-u <109771707+el-u@users.noreply.github.com>
* Cleaned up comments for progressive undernet ROM function, moved index list to field to avoid re-initializing
* Removes improper player-indexed location/item dicts, replaces with world member variables
* Avoids redefining list in progressive undernet ROM function
* Filler items can no longer be generated beyond their specified amounts
* Fixes list copying issue with item frequencies
* Adds BN3 Client Generation back into Launcher settings
* Fixes typos causing huge problems
* Fixed non-relative import for apworld
* Removes custom enum implementation that broke pickle
* Displays message when attempting to load an incorrect ROM, will not attempt to patch it
* Filler items can now only be placed once
* Changes path in setup doc to match Lua path changes
* Fixes file extension for MMBN3 file
* Replaces magic number with reference to value in NetUtils
* Moves victory rules to set_rules. Removes commented out code
* Rewrites Lua script to send block of memory
* Fixes off-by-one error in sending bytes for locations
* Fixes issue with invalid characters in text parsing, and WWW monitor text box parsing
* Moves trade text injection to init so it has access to options
* Attempts to split the text boxes for hinted items
* Trade checks now provide hints if the option is set for them
* Fixes escape character issue for BizHawk 2.9.1
Something in Bizhawk lua parsing changed to dislike the escaped tilde.
I'm not even entirely sure why it was escaped in the first place, but
this should fix the compatibility of it.
* Re-adds desk check that it turns out actually does exist
* Updates requirements to mention bizhawk 2.7 instead of 2.3.1
* Fixes off-by-one error in command byte counts
* Fixes program color indices
* Fixes newline PEP violations
* Reverts an accidental whitespace change made to launcher.py
* Fixes URL formatting on link to settings from setup guide
Co-authored-by: Zach Parks <zach@alliware.com>
* Splits several lines in the readme to avoid excessive length
* Fixes formatting and (hopefully) reduces cringe of joke in setup doc
* Removes unnecessary constructor
* Changes item frequency generation to avoid reusing the same references
Co-authored-by: Zach Parks <zach@alliware.com>
---------
Co-authored-by: el-u <109771707+el-u@users.noreply.github.com>
Co-authored-by: SoldierofOrder <107806872+SoldierofOrder@users.noreply.github.com>
Co-authored-by: Zach Parks <zach@alliware.com>
2023-06-29 18:36:01 +00:00
|
|
|
"mmbn3_options": {
|
|
|
|
"rom_file": "Mega Man Battle Network 3 - Blue Version (USA).gba",
|
|
|
|
"rom_start": True
|
|
|
|
},
|
2023-03-22 14:25:55 +00:00
|
|
|
"adventure_options": {
|
|
|
|
"rom_file": "ADVNTURE.BIN",
|
|
|
|
"display_msgs": True,
|
|
|
|
"rom_start": True,
|
|
|
|
"rom_args": ""
|
|
|
|
},
|
2021-07-09 15:44:24 +00:00
|
|
|
}
|
|
|
|
return options
|
2020-11-28 19:34:29 +00:00
|
|
|
|
2020-11-30 15:43:13 +00:00
|
|
|
|
2022-09-28 21:54:10 +00:00
|
|
|
def update_options(src: dict, dest: dict, filename: str, keys: list) -> OptionsType:
|
2020-11-28 22:51:13 +00:00
|
|
|
for key, value in src.items():
|
|
|
|
new_keys = keys.copy()
|
|
|
|
new_keys.append(key)
|
2020-11-30 15:43:13 +00:00
|
|
|
option_name = '.'.join(new_keys)
|
2020-11-28 22:51:13 +00:00
|
|
|
if key not in dest:
|
|
|
|
dest[key] = value
|
2021-07-30 23:40:27 +00:00
|
|
|
if filename.endswith("options.yaml"):
|
2020-11-30 15:43:13 +00:00
|
|
|
logging.info(f"Warning: {filename} is missing {option_name}")
|
2020-11-28 22:51:13 +00:00
|
|
|
elif isinstance(value, dict):
|
|
|
|
if not isinstance(dest.get(key, None), dict):
|
2021-07-30 23:40:27 +00:00
|
|
|
if filename.endswith("options.yaml"):
|
2020-11-30 15:43:13 +00:00
|
|
|
logging.info(f"Warning: {filename} has {option_name}, but it is not a dictionary. overwriting.")
|
2020-11-28 22:51:13 +00:00
|
|
|
dest[key] = value
|
|
|
|
else:
|
|
|
|
dest[key] = update_options(value, dest[key], filename, new_keys)
|
|
|
|
return dest
|
2020-11-28 19:34:29 +00:00
|
|
|
|
2021-07-30 23:40:27 +00:00
|
|
|
|
2021-07-09 15:44:24 +00:00
|
|
|
@cache_argsless
|
2022-09-28 21:54:10 +00:00
|
|
|
def get_options() -> OptionsType:
|
2022-08-11 22:32:37 +00:00
|
|
|
filenames = ("options.yaml", "host.yaml")
|
2022-09-28 21:54:10 +00:00
|
|
|
locations: typing.List[str] = []
|
2022-08-11 22:32:37 +00:00
|
|
|
if os.path.join(os.getcwd()) != local_path():
|
|
|
|
locations += filenames # use files from cwd only if it's not the local_path
|
|
|
|
locations += [user_path(filename) for filename in filenames]
|
|
|
|
|
|
|
|
for location in locations:
|
|
|
|
if os.path.exists(location):
|
|
|
|
with open(location) as f:
|
|
|
|
options = parse_yaml(f.read())
|
|
|
|
return update_options(get_default_options(), options, location, list())
|
|
|
|
|
|
|
|
raise FileNotFoundError(f"Could not find {filenames[1]} to load options.")
|
2020-04-14 18:22:42 +00:00
|
|
|
|
|
|
|
|
2021-02-25 01:07:28 +00:00
|
|
|
def persistent_store(category: str, key: typing.Any, value: typing.Any):
|
2022-03-31 03:08:15 +00:00
|
|
|
path = user_path("_persistent_storage.yaml")
|
2020-04-24 03:29:02 +00:00
|
|
|
storage: dict = persistent_load()
|
|
|
|
category = storage.setdefault(category, {})
|
|
|
|
category[key] = value
|
|
|
|
with open(path, "wt") as f:
|
2022-08-11 22:32:37 +00:00
|
|
|
f.write(dump(storage, Dumper=Dumper))
|
2020-04-24 03:29:02 +00:00
|
|
|
|
|
|
|
|
2022-08-11 22:32:37 +00:00
|
|
|
def persistent_load() -> typing.Dict[str, dict]:
|
2020-06-04 19:27:29 +00:00
|
|
|
storage = getattr(persistent_load, "storage", None)
|
|
|
|
if storage:
|
|
|
|
return storage
|
2022-03-31 03:08:15 +00:00
|
|
|
path = user_path("_persistent_storage.yaml")
|
2020-04-24 03:29:02 +00:00
|
|
|
storage: dict = {}
|
|
|
|
if os.path.exists(path):
|
|
|
|
try:
|
|
|
|
with open(path, "r") as f:
|
2020-07-05 00:06:00 +00:00
|
|
|
storage = unsafe_parse_yaml(f.read())
|
2020-04-24 03:29:02 +00:00
|
|
|
except Exception as e:
|
|
|
|
logging.debug(f"Could not read store: {e}")
|
2020-04-30 05:42:26 +00:00
|
|
|
if storage is None:
|
|
|
|
storage = {}
|
2020-06-04 19:27:29 +00:00
|
|
|
persistent_load.storage = storage
|
2020-04-24 03:29:02 +00:00
|
|
|
return storage
|
|
|
|
|
|
|
|
|
2023-03-20 16:01:08 +00:00
|
|
|
def get_file_safe_name(name: str) -> str:
|
|
|
|
return "".join(c for c in name if c not in '<>:"/\\|?*')
|
|
|
|
|
|
|
|
|
|
|
|
def load_data_package_for_checksum(game: str, checksum: typing.Optional[str]) -> Dict[str, Any]:
|
|
|
|
if checksum and game:
|
|
|
|
if checksum != get_file_safe_name(checksum):
|
|
|
|
raise ValueError(f"Bad symbols in checksum: {checksum}")
|
|
|
|
path = cache_path("datapackage", get_file_safe_name(game), f"{checksum}.json")
|
|
|
|
if os.path.exists(path):
|
|
|
|
try:
|
|
|
|
with open(path, "r", encoding="utf-8-sig") as f:
|
|
|
|
return json.load(f)
|
|
|
|
except Exception as e:
|
|
|
|
logging.debug(f"Could not load data package: {e}")
|
|
|
|
|
|
|
|
# fall back to old cache
|
|
|
|
cache = persistent_load().get("datapackage", {}).get("games", {}).get(game, {})
|
|
|
|
if cache.get("checksum") == checksum:
|
|
|
|
return cache
|
|
|
|
|
|
|
|
# cache does not match
|
|
|
|
return {}
|
|
|
|
|
|
|
|
|
|
|
|
def store_data_package_for_checksum(game: str, data: typing.Dict[str, Any]) -> None:
|
|
|
|
checksum = data.get("checksum")
|
|
|
|
if checksum and game:
|
|
|
|
if checksum != get_file_safe_name(checksum):
|
|
|
|
raise ValueError(f"Bad symbols in checksum: {checksum}")
|
|
|
|
game_folder = cache_path("datapackage", get_file_safe_name(game))
|
|
|
|
os.makedirs(game_folder, exist_ok=True)
|
|
|
|
try:
|
|
|
|
with open(os.path.join(game_folder, f"{checksum}.json"), "w", encoding="utf-8-sig") as f:
|
|
|
|
json.dump(data, f, ensure_ascii=False, separators=(",", ":"))
|
|
|
|
except Exception as e:
|
|
|
|
logging.debug(f"Could not store data package: {e}")
|
|
|
|
|
|
|
|
|
2022-09-28 21:54:10 +00:00
|
|
|
def get_adjuster_settings(game_name: str) -> typing.Dict[str, typing.Any]:
|
2022-08-11 22:32:37 +00:00
|
|
|
adjuster_settings = persistent_load().get("adjuster", {}).get(game_name, {})
|
2022-01-20 03:19:58 +00:00
|
|
|
return adjuster_settings
|
2020-06-07 19:04:33 +00:00
|
|
|
|
2021-07-31 13:13:55 +00:00
|
|
|
|
2021-07-09 15:44:24 +00:00
|
|
|
@cache_argsless
|
2020-06-04 19:27:29 +00:00
|
|
|
def get_unique_identifier():
|
|
|
|
uuid = persistent_load().get("client", {}).get("uuid", None)
|
|
|
|
if uuid:
|
|
|
|
return uuid
|
|
|
|
|
|
|
|
import uuid
|
|
|
|
uuid = uuid.getnode()
|
|
|
|
persistent_store("client", "uuid", uuid)
|
|
|
|
return uuid
|
2020-09-08 23:41:37 +00:00
|
|
|
|
|
|
|
|
2022-08-11 22:32:37 +00:00
|
|
|
safe_builtins = frozenset((
|
2020-09-08 23:41:37 +00:00
|
|
|
'set',
|
|
|
|
'frozenset',
|
2022-08-11 22:32:37 +00:00
|
|
|
))
|
2020-09-08 23:41:37 +00:00
|
|
|
|
|
|
|
|
|
|
|
class RestrictedUnpickler(pickle.Unpickler):
|
2021-09-17 23:02:26 +00:00
|
|
|
def __init__(self, *args, **kwargs):
|
|
|
|
super(RestrictedUnpickler, self).__init__(*args, **kwargs)
|
|
|
|
self.options_module = importlib.import_module("Options")
|
|
|
|
self.net_utils_module = importlib.import_module("NetUtils")
|
2021-09-23 00:29:24 +00:00
|
|
|
self.generic_properties_module = importlib.import_module("worlds.generic")
|
2021-09-17 23:02:26 +00:00
|
|
|
|
2020-09-08 23:41:37 +00:00
|
|
|
def find_class(self, module, name):
|
|
|
|
if module == "builtins" and name in safe_builtins:
|
|
|
|
return getattr(builtins, name)
|
2021-09-17 23:02:26 +00:00
|
|
|
# used by MultiServer -> savegame/multidata
|
2022-01-30 12:57:12 +00:00
|
|
|
if module == "NetUtils" and name in {"NetworkItem", "ClientStatus", "Hint", "SlotType", "NetworkSlot"}:
|
2021-09-17 23:02:26 +00:00
|
|
|
return getattr(self.net_utils_module, name)
|
2021-09-23 00:29:24 +00:00
|
|
|
# Options and Plando are unpickled by WebHost -> Generate
|
|
|
|
if module == "worlds.generic" and name in {"PlandoItem", "PlandoConnection"}:
|
|
|
|
return getattr(self.generic_properties_module, name)
|
2022-09-28 21:54:10 +00:00
|
|
|
# pep 8 specifies that modules should have "all-lowercase names" (options, not Options)
|
|
|
|
if module.lower().endswith("options"):
|
2021-09-17 23:02:26 +00:00
|
|
|
if module == "Options":
|
|
|
|
mod = self.options_module
|
|
|
|
else:
|
|
|
|
mod = importlib.import_module(module)
|
|
|
|
obj = getattr(mod, name)
|
|
|
|
if issubclass(obj, self.options_module.Option):
|
2021-05-16 20:59:45 +00:00
|
|
|
return obj
|
2020-09-08 23:41:37 +00:00
|
|
|
# Forbid everything else.
|
2022-08-11 22:32:37 +00:00
|
|
|
raise pickle.UnpicklingError(f"global '{module}.{name}' is forbidden")
|
2020-09-08 23:41:37 +00:00
|
|
|
|
|
|
|
|
|
|
|
def restricted_loads(s):
|
|
|
|
"""Helper function analogous to pickle.loads()."""
|
2021-07-07 08:14:58 +00:00
|
|
|
return RestrictedUnpickler(io.BytesIO(s)).load()
|
|
|
|
|
2021-07-09 15:44:24 +00:00
|
|
|
|
2023-05-02 06:23:39 +00:00
|
|
|
class ByValue:
|
|
|
|
"""
|
|
|
|
Mixin for enums to pickle value instead of name (restores pre-3.11 behavior). Use as left-most parent.
|
|
|
|
See https://github.com/python/cpython/pull/26658 for why this exists.
|
|
|
|
"""
|
|
|
|
def __reduce_ex__(self, prot):
|
|
|
|
return self.__class__, (self._value_, )
|
|
|
|
|
|
|
|
|
2021-07-07 08:14:58 +00:00
|
|
|
class KeyedDefaultDict(collections.defaultdict):
|
2022-08-12 04:52:01 +00:00
|
|
|
"""defaultdict variant that uses the missing key as argument to default_factory"""
|
|
|
|
default_factory: typing.Callable[[typing.Any], typing.Any]
|
|
|
|
|
2021-07-07 08:14:58 +00:00
|
|
|
def __missing__(self, key):
|
|
|
|
self[key] = value = self.default_factory(key)
|
2021-10-16 17:40:27 +00:00
|
|
|
return value
|
|
|
|
|
|
|
|
|
|
|
|
def get_text_between(text: str, start: str, end: str) -> str:
|
|
|
|
return text[text.index(start) + len(start): text.rindex(end)]
|
2021-11-10 14:35:43 +00:00
|
|
|
|
|
|
|
|
2022-08-17 22:27:37 +00:00
|
|
|
def get_text_after(text: str, start: str) -> str:
|
|
|
|
return text[text.index(start) + len(start):]
|
|
|
|
|
|
|
|
|
2021-11-10 14:35:43 +00:00
|
|
|
loglevel_mapping = {'error': logging.ERROR, 'info': logging.INFO, 'warning': logging.WARNING, 'debug': logging.DEBUG}
|
|
|
|
|
|
|
|
|
|
|
|
def init_logging(name: str, loglevel: typing.Union[str, int] = logging.INFO, write_mode: str = "w",
|
2022-06-07 22:34:45 +00:00
|
|
|
log_format: str = "[%(name)s at %(asctime)s]: %(message)s",
|
|
|
|
exception_logger: typing.Optional[str] = None):
|
2022-11-17 20:27:44 +00:00
|
|
|
import datetime
|
2021-11-10 14:35:43 +00:00
|
|
|
loglevel: int = loglevel_mapping.get(loglevel, loglevel)
|
2022-03-31 03:08:15 +00:00
|
|
|
log_folder = user_path("logs")
|
2021-11-10 14:35:43 +00:00
|
|
|
os.makedirs(log_folder, exist_ok=True)
|
|
|
|
root_logger = logging.getLogger()
|
|
|
|
for handler in root_logger.handlers[:]:
|
|
|
|
root_logger.removeHandler(handler)
|
|
|
|
handler.close()
|
|
|
|
root_logger.setLevel(loglevel)
|
2023-05-20 17:18:25 +00:00
|
|
|
logging.getLogger("websockets").setLevel(loglevel) # make sure level is applied for websockets
|
2022-11-17 20:27:44 +00:00
|
|
|
if "a" not in write_mode:
|
|
|
|
name += f"_{datetime.datetime.now().strftime('%Y_%m_%d_%H_%M_%S')}"
|
2021-11-10 14:35:43 +00:00
|
|
|
file_handler = logging.FileHandler(
|
|
|
|
os.path.join(log_folder, f"{name}.txt"),
|
|
|
|
write_mode,
|
|
|
|
encoding="utf-8-sig")
|
|
|
|
file_handler.setFormatter(logging.Formatter(log_format))
|
|
|
|
root_logger.addHandler(file_handler)
|
|
|
|
if sys.stdout:
|
|
|
|
root_logger.addHandler(
|
|
|
|
logging.StreamHandler(sys.stdout)
|
|
|
|
)
|
2021-11-17 21:46:32 +00:00
|
|
|
|
|
|
|
# Relay unhandled exceptions to logger.
|
|
|
|
if not getattr(sys.excepthook, "_wrapped", False): # skip if already modified
|
|
|
|
orig_hook = sys.excepthook
|
|
|
|
|
|
|
|
def handle_exception(exc_type, exc_value, exc_traceback):
|
|
|
|
if issubclass(exc_type, KeyboardInterrupt):
|
|
|
|
sys.__excepthook__(exc_type, exc_value, exc_traceback)
|
|
|
|
return
|
|
|
|
logging.getLogger(exception_logger).exception("Uncaught exception",
|
|
|
|
exc_info=(exc_type, exc_value, exc_traceback))
|
|
|
|
return orig_hook(exc_type, exc_value, exc_traceback)
|
|
|
|
|
|
|
|
handle_exception._wrapped = True
|
|
|
|
|
|
|
|
sys.excepthook = handle_exception
|
2021-11-28 03:06:30 +00:00
|
|
|
|
2022-11-17 20:27:44 +00:00
|
|
|
def _cleanup():
|
|
|
|
for file in os.scandir(log_folder):
|
|
|
|
if file.name.endswith(".txt"):
|
|
|
|
last_change = datetime.datetime.fromtimestamp(file.stat().st_mtime)
|
|
|
|
if datetime.datetime.now() - last_change > datetime.timedelta(days=7):
|
|
|
|
try:
|
|
|
|
os.unlink(file.path)
|
|
|
|
except Exception as e:
|
|
|
|
logging.exception(e)
|
|
|
|
else:
|
2023-01-23 01:23:16 +00:00
|
|
|
logging.debug(f"Deleted old logfile {file.path}")
|
2022-11-17 20:27:44 +00:00
|
|
|
import threading
|
|
|
|
threading.Thread(target=_cleanup, name="LogCleaner").start()
|
2022-11-28 01:52:36 +00:00
|
|
|
import platform
|
|
|
|
logging.info(
|
|
|
|
f"Archipelago ({__version__}) logging initialized"
|
|
|
|
f" on {platform.platform()}"
|
|
|
|
f" running Python {sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}"
|
|
|
|
)
|
2022-06-07 22:34:45 +00:00
|
|
|
|
2021-11-28 03:06:30 +00:00
|
|
|
|
|
|
|
def stream_input(stream, queue):
|
|
|
|
def queuer():
|
|
|
|
while 1:
|
2022-08-03 12:53:14 +00:00
|
|
|
try:
|
|
|
|
text = stream.readline().strip()
|
|
|
|
except UnicodeDecodeError as e:
|
|
|
|
logging.exception(e)
|
|
|
|
else:
|
|
|
|
if text:
|
|
|
|
queue.put_nowait(text)
|
2021-11-28 03:06:30 +00:00
|
|
|
|
|
|
|
from threading import Thread
|
|
|
|
thread = Thread(target=queuer, name=f"Stream handler for {stream.name}", daemon=True)
|
|
|
|
thread.start()
|
|
|
|
return thread
|
2022-01-18 07:23:38 +00:00
|
|
|
|
|
|
|
|
2022-08-11 22:32:37 +00:00
|
|
|
def tkinter_center_window(window: "tkinter.Tk") -> None:
|
2022-01-20 03:19:58 +00:00
|
|
|
window.update()
|
2022-08-11 22:32:37 +00:00
|
|
|
x = int(window.winfo_screenwidth() / 2 - window.winfo_reqwidth() / 2)
|
|
|
|
y = int(window.winfo_screenheight() / 2 - window.winfo_reqheight() / 2)
|
|
|
|
window.geometry(f"+{x}+{y}")
|
2022-01-20 03:19:58 +00:00
|
|
|
|
2022-02-24 03:47:01 +00:00
|
|
|
|
2022-01-18 07:23:38 +00:00
|
|
|
class VersionException(Exception):
|
|
|
|
pass
|
2022-01-20 03:19:58 +00:00
|
|
|
|
2022-02-24 03:47:01 +00:00
|
|
|
|
2022-06-21 18:50:40 +00:00
|
|
|
def chaining_prefix(index: int, labels: typing.Tuple[str]) -> str:
|
|
|
|
text = ""
|
|
|
|
max_label = len(labels) - 1
|
|
|
|
while index > max_label:
|
|
|
|
text += labels[-1]
|
|
|
|
index -= max_label
|
|
|
|
return labels[index] + text
|
|
|
|
|
|
|
|
|
2022-04-30 02:39:08 +00:00
|
|
|
# noinspection PyPep8Naming
|
2022-08-11 22:46:11 +00:00
|
|
|
def format_SI_prefix(value, power=1000, power_labels=("", "k", "M", "G", "T", "P", "E", "Z", "Y")) -> str:
|
2022-06-21 18:50:40 +00:00
|
|
|
"""Formats a value into a value + metric/si prefix. More info at https://en.wikipedia.org/wiki/Metric_prefix"""
|
2022-08-12 21:02:56 +00:00
|
|
|
import decimal
|
2022-02-24 03:47:01 +00:00
|
|
|
n = 0
|
2022-06-21 18:50:40 +00:00
|
|
|
value = decimal.Decimal(value)
|
2022-08-11 22:46:11 +00:00
|
|
|
limit = power - decimal.Decimal("0.005")
|
|
|
|
while value >= limit:
|
2022-02-24 03:47:01 +00:00
|
|
|
value /= power
|
|
|
|
n += 1
|
2022-06-21 18:50:40 +00:00
|
|
|
|
|
|
|
return f"{value.quantize(decimal.Decimal('1.00'))} {chaining_prefix(n, power_labels)}"
|
2022-05-09 05:18:50 +00:00
|
|
|
|
|
|
|
|
2022-05-09 15:03:16 +00:00
|
|
|
def get_fuzzy_results(input_word: str, wordlist: typing.Sequence[str], limit: typing.Optional[int] = None) \
|
|
|
|
-> typing.List[typing.Tuple[str, int]]:
|
2022-08-11 22:32:37 +00:00
|
|
|
import jellyfish
|
|
|
|
|
|
|
|
def get_fuzzy_ratio(word1: str, word2: str) -> float:
|
|
|
|
return (1 - jellyfish.damerau_levenshtein_distance(word1.lower(), word2.lower())
|
|
|
|
/ max(len(word1), len(word2)))
|
|
|
|
|
2022-05-09 15:03:16 +00:00
|
|
|
limit: int = limit if limit else len(wordlist)
|
|
|
|
return list(
|
|
|
|
map(
|
|
|
|
lambda container: (container[0], int(container[1]*100)), # convert up to limit to int %
|
|
|
|
sorted(
|
|
|
|
map(lambda candidate:
|
|
|
|
(candidate, get_fuzzy_ratio(input_word, candidate)),
|
|
|
|
wordlist),
|
|
|
|
key=lambda element: element[1],
|
|
|
|
reverse=True)[0:limit]
|
|
|
|
)
|
|
|
|
)
|
2022-06-04 15:02:02 +00:00
|
|
|
|
|
|
|
|
2022-06-04 16:36:50 +00:00
|
|
|
def open_filename(title: str, filetypes: typing.Sequence[typing.Tuple[str, typing.Sequence[str]]]) \
|
|
|
|
-> typing.Optional[str]:
|
|
|
|
def run(*args: str):
|
2022-08-11 22:32:37 +00:00
|
|
|
return subprocess.run(args, capture_output=True, text=True).stdout.split("\n", 1)[0] or None
|
2022-06-04 16:36:50 +00:00
|
|
|
|
|
|
|
if is_linux:
|
|
|
|
# prefer native dialog
|
2022-08-11 22:32:37 +00:00
|
|
|
from shutil import which
|
|
|
|
kdialog = which("kdialog")
|
2022-06-04 16:36:50 +00:00
|
|
|
if kdialog:
|
|
|
|
k_filters = '|'.join((f'{text} (*{" *".join(ext)})' for (text, ext) in filetypes))
|
2022-08-11 22:32:37 +00:00
|
|
|
return run(kdialog, f"--title={title}", "--getopenfilename", ".", k_filters)
|
|
|
|
zenity = which("zenity")
|
2022-06-04 16:36:50 +00:00
|
|
|
if zenity:
|
|
|
|
z_filters = (f'--file-filter={text} ({", ".join(ext)}) | *{" *".join(ext)}' for (text, ext) in filetypes)
|
2022-08-11 22:32:37 +00:00
|
|
|
return run(zenity, f"--title={title}", "--file-selection", *z_filters)
|
2022-06-04 16:36:50 +00:00
|
|
|
|
|
|
|
# fall back to tk
|
|
|
|
try:
|
|
|
|
import tkinter
|
|
|
|
import tkinter.filedialog
|
|
|
|
except Exception as e:
|
|
|
|
logging.error('Could not load tkinter, which is likely not installed. '
|
|
|
|
f'This attempt was made because open_filename was used for "{title}".')
|
|
|
|
raise e
|
|
|
|
else:
|
|
|
|
root = tkinter.Tk()
|
|
|
|
root.withdraw()
|
|
|
|
return tkinter.filedialog.askopenfilename(title=title, filetypes=((t[0], ' '.join(t[1])) for t in filetypes))
|
|
|
|
|
|
|
|
|
2022-06-04 15:02:02 +00:00
|
|
|
def messagebox(title: str, text: str, error: bool = False) -> None:
|
2022-06-23 17:26:30 +00:00
|
|
|
def run(*args: str):
|
2022-08-11 22:32:37 +00:00
|
|
|
return subprocess.run(args, capture_output=True, text=True).stdout.split("\n", 1)[0] or None
|
2022-06-23 17:26:30 +00:00
|
|
|
|
2022-06-04 15:02:02 +00:00
|
|
|
def is_kivy_running():
|
2022-08-11 22:32:37 +00:00
|
|
|
if "kivy" in sys.modules:
|
2022-06-04 15:02:02 +00:00
|
|
|
from kivy.app import App
|
|
|
|
return App.get_running_app() is not None
|
|
|
|
return False
|
|
|
|
|
|
|
|
if is_kivy_running():
|
|
|
|
from kvui import MessageBox
|
|
|
|
MessageBox(title, text, error).open()
|
|
|
|
return
|
|
|
|
|
2022-08-11 22:32:37 +00:00
|
|
|
if is_linux and "tkinter" not in sys.modules:
|
2022-06-23 17:26:30 +00:00
|
|
|
# prefer native dialog
|
2022-08-11 22:32:37 +00:00
|
|
|
from shutil import which
|
|
|
|
kdialog = which("kdialog")
|
2022-06-23 17:26:30 +00:00
|
|
|
if kdialog:
|
2022-08-11 22:32:37 +00:00
|
|
|
return run(kdialog, f"--title={title}", "--error" if error else "--msgbox", text)
|
|
|
|
zenity = which("zenity")
|
2022-06-23 17:26:30 +00:00
|
|
|
if zenity:
|
2022-08-11 22:32:37 +00:00
|
|
|
return run(zenity, f"--title={title}", f"--text={text}", "--error" if error else "--info")
|
2022-06-23 17:26:30 +00:00
|
|
|
|
2022-06-04 15:02:02 +00:00
|
|
|
# fall back to tk
|
|
|
|
try:
|
|
|
|
import tkinter
|
|
|
|
from tkinter.messagebox import showerror, showinfo
|
|
|
|
except Exception as e:
|
|
|
|
logging.error('Could not load tkinter, which is likely not installed. '
|
|
|
|
f'This attempt was made because messagebox was used for "{title}".')
|
|
|
|
raise e
|
|
|
|
else:
|
|
|
|
root = tkinter.Tk()
|
|
|
|
root.withdraw()
|
|
|
|
showerror(title, text) if error else showinfo(title, text)
|
|
|
|
root.update()
|
2022-08-09 20:21:45 +00:00
|
|
|
|
|
|
|
|
|
|
|
def title_sorted(data: typing.Sequence, key=None, ignore: typing.Set = frozenset(("a", "the"))):
|
|
|
|
"""Sorts a sequence of text ignoring typical articles like "a" or "the" in the beginning."""
|
2023-02-17 18:16:37 +00:00
|
|
|
def sorter(element: Union[str, Dict[str, Any]]) -> str:
|
|
|
|
if (not isinstance(element, str)):
|
|
|
|
element = element["title"]
|
|
|
|
|
2022-08-09 20:21:45 +00:00
|
|
|
parts = element.split(maxsplit=1)
|
|
|
|
if parts[0].lower() in ignore:
|
2022-08-26 14:44:09 +00:00
|
|
|
return parts[1].lower()
|
2022-08-09 20:21:45 +00:00
|
|
|
else:
|
2022-08-26 14:44:09 +00:00
|
|
|
return element.lower()
|
2022-08-09 20:21:45 +00:00
|
|
|
return sorted(data, key=lambda i: sorter(key(i)) if key else sorter(i))
|
2022-09-29 22:36:30 +00:00
|
|
|
|
|
|
|
|
|
|
|
def read_snes_rom(stream: BinaryIO, strip_header: bool = True) -> bytearray:
|
|
|
|
"""Reads rom into bytearray and optionally strips off any smc header"""
|
|
|
|
buffer = bytearray(stream.read())
|
|
|
|
if strip_header and len(buffer) % 0x400 == 0x200:
|
|
|
|
return buffer[0x200:]
|
|
|
|
return buffer
|
2022-11-02 14:51:35 +00:00
|
|
|
|
|
|
|
|
2023-06-29 13:06:58 +00:00
|
|
|
_faf_tasks: "Set[asyncio.Task[typing.Any]]" = set()
|
2022-11-02 14:51:35 +00:00
|
|
|
|
|
|
|
|
2023-06-29 13:06:58 +00:00
|
|
|
def async_start(co: Coroutine[None, None, typing.Any], name: Optional[str] = None) -> None:
|
2022-11-02 14:51:35 +00:00
|
|
|
"""
|
|
|
|
Use this to start a task when you don't keep a reference to it or immediately await it,
|
|
|
|
to prevent early garbage collection. "fire-and-forget"
|
|
|
|
"""
|
|
|
|
# https://docs.python.org/3.10/library/asyncio-task.html#asyncio.create_task
|
|
|
|
# Python docs:
|
|
|
|
# ```
|
|
|
|
# Important: Save a reference to the result of [asyncio.create_task],
|
|
|
|
# to avoid a task disappearing mid-execution.
|
|
|
|
# ```
|
|
|
|
# This implementation follows the pattern given in that documentation.
|
|
|
|
|
2023-06-29 13:06:58 +00:00
|
|
|
task: asyncio.Task[typing.Any] = asyncio.create_task(co, name=name)
|
2022-11-02 14:51:35 +00:00
|
|
|
_faf_tasks.add(task)
|
|
|
|
task.add_done_callback(_faf_tasks.discard)
|
2023-06-19 07:57:17 +00:00
|
|
|
|
|
|
|
|
|
|
|
def deprecate(message: str):
|
|
|
|
if __debug__:
|
|
|
|
raise Exception(message)
|
|
|
|
import warnings
|
|
|
|
warnings.warn(message)
|
2023-06-25 00:24:43 +00:00
|
|
|
|
|
|
|
def _extend_freeze_support() -> None:
|
|
|
|
"""Extend multiprocessing.freeze_support() to also work on Non-Windows for spawn."""
|
|
|
|
# upstream issue: https://github.com/python/cpython/issues/76327
|
|
|
|
# code based on https://github.com/pyinstaller/pyinstaller/blob/develop/PyInstaller/hooks/rthooks/pyi_rth_multiprocessing.py#L26
|
|
|
|
import multiprocessing
|
|
|
|
import multiprocessing.spawn
|
|
|
|
|
|
|
|
def _freeze_support() -> None:
|
|
|
|
"""Minimal freeze_support. Only apply this if frozen."""
|
|
|
|
from subprocess import _args_from_interpreter_flags
|
|
|
|
|
|
|
|
# Prevent `spawn` from trying to read `__main__` in from the main script
|
|
|
|
multiprocessing.process.ORIGINAL_DIR = None
|
|
|
|
|
|
|
|
# Handle the first process that MP will create
|
|
|
|
if (
|
|
|
|
len(sys.argv) >= 2 and sys.argv[-2] == '-c' and sys.argv[-1].startswith((
|
|
|
|
'from multiprocessing.semaphore_tracker import main', # Py<3.8
|
|
|
|
'from multiprocessing.resource_tracker import main', # Py>=3.8
|
|
|
|
'from multiprocessing.forkserver import main'
|
|
|
|
)) and set(sys.argv[1:-2]) == set(_args_from_interpreter_flags())
|
|
|
|
):
|
|
|
|
exec(sys.argv[-1])
|
|
|
|
sys.exit()
|
|
|
|
|
|
|
|
# Handle the second process that MP will create
|
|
|
|
if multiprocessing.spawn.is_forking(sys.argv):
|
|
|
|
kwargs = {}
|
|
|
|
for arg in sys.argv[2:]:
|
|
|
|
name, value = arg.split('=')
|
|
|
|
if value == 'None':
|
|
|
|
kwargs[name] = None
|
|
|
|
else:
|
|
|
|
kwargs[name] = int(value)
|
|
|
|
multiprocessing.spawn.spawn_main(**kwargs)
|
|
|
|
sys.exit()
|
|
|
|
|
|
|
|
if not is_windows and is_frozen():
|
|
|
|
multiprocessing.freeze_support = multiprocessing.spawn.freeze_support = _freeze_support
|
|
|
|
|
|
|
|
|
|
|
|
def freeze_support() -> None:
|
|
|
|
"""This behaves like multiprocessing.freeze_support but also works on Non-Windows."""
|
|
|
|
import multiprocessing
|
|
|
|
_extend_freeze_support()
|
|
|
|
multiprocessing.freeze_support()
|