mirror of
https://github.com/ArchipelagoMW/Archipelago.git
synced 2026-03-22 15:45:04 -07:00
1.3
This commit is contained in:
@@ -3,7 +3,7 @@ import Utils
|
||||
import websockets
|
||||
import functools
|
||||
from copy import deepcopy
|
||||
from typing import List, Any, Iterable
|
||||
from typing import List, Any, Iterable, Dict
|
||||
from NetUtils import decode, encode, JSONtoTextParser, JSONMessagePart, NetworkItem
|
||||
from MultiServer import Endpoint
|
||||
from CommonClient import CommonContext, gui_enabled, ClientCommandProcessor, logger, \
|
||||
@@ -70,12 +70,18 @@ class AHITContext(CommonContext):
|
||||
await self.endpoint.socket.send(msgs)
|
||||
return True
|
||||
|
||||
async def disconnect(self, allow_autoreconnect: bool = False):
|
||||
await super().disconnect(allow_autoreconnect)
|
||||
|
||||
async def disconnect_proxy(self):
|
||||
if self.endpoint and not self.endpoint.socket.closed:
|
||||
await self.endpoint.socket.close()
|
||||
if self.proxy_task is not None:
|
||||
await self.proxy_task
|
||||
|
||||
def is_connected(self) -> bool:
|
||||
return self.server and self.server.socket.open
|
||||
|
||||
def is_proxy_connected(self) -> bool:
|
||||
return self.endpoint and self.endpoint.socket.open
|
||||
|
||||
@@ -91,6 +97,10 @@ class AHITContext(CommonContext):
|
||||
logger.info(text)
|
||||
|
||||
def update_items(self):
|
||||
# just to be safe - we might still have an inventory from a different room
|
||||
if not self.is_connected():
|
||||
return
|
||||
|
||||
self.server_msgs.append(encode([{"cmd": "ReceivedItems", "index": 0, "items": self.full_inventory}]))
|
||||
|
||||
def on_package(self, cmd: str, args: dict):
|
||||
@@ -118,6 +128,10 @@ class AHITContext(CommonContext):
|
||||
if cmd != "PrintJSON":
|
||||
self.server_msgs.append(encode([args]))
|
||||
|
||||
# def on_deathlink(self, data: Dict[str, Any]):
|
||||
# self.server_msgs.append(encode([data]))
|
||||
# super().on_deathlink(data)
|
||||
|
||||
def run_gui(self):
|
||||
from kvui import GameManager
|
||||
|
||||
@@ -147,13 +161,17 @@ async def proxy(websocket, path: str = "/", ctx: AHITContext = None):
|
||||
break
|
||||
|
||||
if ctx.seed_name:
|
||||
seed = msg.get("seed", "")
|
||||
if seed != "" and seed != ctx.seed_name:
|
||||
seed_name = msg.get("seed_name", "")
|
||||
if seed_name != "" and seed_name != ctx.seed_name:
|
||||
logger.info("Aborting proxy connection: seed mismatch from save file")
|
||||
logger.info(f"Expected: {ctx.seed_name}, got: {seed_name}")
|
||||
text = encode([{"cmd": "PrintJSON",
|
||||
"data": [{"text": "Connection aborted - save file to seed mismatch"}]}])
|
||||
await ctx.send_msgs_proxy(text)
|
||||
await ctx.disconnect_proxy()
|
||||
break
|
||||
|
||||
if ctx.connected_msg:
|
||||
if ctx.connected_msg and ctx.is_connected():
|
||||
await ctx.send_msgs_proxy(ctx.connected_msg)
|
||||
ctx.update_items()
|
||||
continue
|
||||
@@ -174,7 +192,7 @@ async def proxy(websocket, path: str = "/", ctx: AHITContext = None):
|
||||
|
||||
|
||||
async def on_client_connected(ctx: AHITContext):
|
||||
if ctx.room_info:
|
||||
if ctx.room_info and ctx.is_connected():
|
||||
await ctx.send_msgs_proxy(ctx.room_info)
|
||||
else:
|
||||
ctx.awaiting_info = True
|
||||
@@ -186,7 +204,8 @@ async def main():
|
||||
|
||||
ctx = AHITContext(args.connect, args.password)
|
||||
logger.info("Starting A Hat in Time proxy server")
|
||||
ctx.proxy = websockets.serve(functools.partial(proxy, ctx=ctx), host="localhost", port=11311)
|
||||
ctx.proxy = websockets.serve(functools.partial(proxy, ctx=ctx),
|
||||
host="localhost", port=11311, ping_timeout=999999, ping_interval=999999)
|
||||
ctx.proxy_task = asyncio.create_task(proxy_loop(ctx), name="ProxyLoop")
|
||||
|
||||
if gui_enabled:
|
||||
|
||||
BIN
data/yatta.ico
Normal file
BIN
data/yatta.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 149 KiB |
BIN
data/yatta.png
Normal file
BIN
data/yatta.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 34 KiB |
642
setup-ahitclient.py
Normal file
642
setup-ahitclient.py
Normal file
@@ -0,0 +1,642 @@
|
||||
import base64
|
||||
import datetime
|
||||
import os
|
||||
import platform
|
||||
import shutil
|
||||
import sys
|
||||
import sysconfig
|
||||
import typing
|
||||
import warnings
|
||||
import zipfile
|
||||
import urllib.request
|
||||
import io
|
||||
import json
|
||||
import threading
|
||||
import subprocess
|
||||
|
||||
from collections.abc import Iterable
|
||||
from hashlib import sha3_512
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
# This is a bit jank. We need cx-Freeze to be able to run anything from this script, so install it
|
||||
try:
|
||||
requirement = 'cx-Freeze>=6.15.2'
|
||||
import pkg_resources
|
||||
try:
|
||||
pkg_resources.require(requirement)
|
||||
install_cx_freeze = False
|
||||
except pkg_resources.ResolutionError:
|
||||
install_cx_freeze = True
|
||||
except ImportError:
|
||||
install_cx_freeze = True
|
||||
pkg_resources = None # type: ignore [assignment]
|
||||
|
||||
if install_cx_freeze:
|
||||
# check if pip is available
|
||||
try:
|
||||
import pip # noqa: F401
|
||||
except ImportError:
|
||||
raise RuntimeError("pip not available. Please install pip.")
|
||||
# install and import cx_freeze
|
||||
if '--yes' not in sys.argv and '-y' not in sys.argv:
|
||||
input(f'Requirement {requirement} is not satisfied, press enter to install it')
|
||||
subprocess.call([sys.executable, '-m', 'pip', 'install', requirement, '--upgrade'])
|
||||
import pkg_resources
|
||||
|
||||
import cx_Freeze
|
||||
|
||||
# .build only exists if cx-Freeze is the right version, so we have to update/install that first before this line
|
||||
import setuptools.command.build
|
||||
|
||||
if __name__ == "__main__":
|
||||
# need to run this early to import from Utils and Launcher
|
||||
# TODO: move stuff to not require this
|
||||
import ModuleUpdate
|
||||
ModuleUpdate.update(yes="--yes" in sys.argv or "-y" in sys.argv)
|
||||
ModuleUpdate.update_ran = False # restore for later
|
||||
|
||||
from worlds.LauncherComponents import components, icon_paths
|
||||
from Utils import version_tuple, is_windows, is_linux
|
||||
from Cython.Build import cythonize
|
||||
|
||||
|
||||
# On Python < 3.10 LogicMixin is not currently supported.
|
||||
non_apworlds: set = {
|
||||
"A Link to the Past",
|
||||
"Adventure",
|
||||
"ArchipIDLE",
|
||||
"Archipelago",
|
||||
"ChecksFinder",
|
||||
"Clique",
|
||||
"DLCQuest",
|
||||
"Final Fantasy",
|
||||
"Hylics 2",
|
||||
"Kingdom Hearts 2",
|
||||
"Lufia II Ancient Cave",
|
||||
"Meritous",
|
||||
"Ocarina of Time",
|
||||
"Overcooked! 2",
|
||||
"Raft",
|
||||
"Secret of Evermore",
|
||||
"Slay the Spire",
|
||||
"Starcraft 2 Wings of Liberty",
|
||||
"Sudoku",
|
||||
"Super Mario 64",
|
||||
"VVVVVV",
|
||||
"Wargroove",
|
||||
"Zillion",
|
||||
}
|
||||
|
||||
# LogicMixin is broken before 3.10 import revamp
|
||||
if sys.version_info < (3,10):
|
||||
non_apworlds.add("Hollow Knight")
|
||||
|
||||
def download_SNI():
|
||||
print("Updating SNI")
|
||||
machine_to_go = {
|
||||
"x86_64": "amd64",
|
||||
"aarch64": "arm64",
|
||||
"armv7l": "arm"
|
||||
}
|
||||
platform_name = platform.system().lower()
|
||||
machine_name = platform.machine().lower()
|
||||
# force amd64 on macos until we have universal2 sni, otherwise resolve to GOARCH
|
||||
machine_name = "amd64" if platform_name == "darwin" else machine_to_go.get(machine_name, machine_name)
|
||||
with urllib.request.urlopen("https://api.github.com/repos/alttpo/sni/releases/latest") as request:
|
||||
data = json.load(request)
|
||||
files = data["assets"]
|
||||
|
||||
source_url = None
|
||||
|
||||
for file in files:
|
||||
download_url: str = file["browser_download_url"]
|
||||
machine_match = download_url.rsplit("-", 1)[1].split(".", 1)[0] == machine_name
|
||||
if platform_name in download_url and machine_match:
|
||||
# prefer "many" builds
|
||||
if "many" in download_url:
|
||||
source_url = download_url
|
||||
break
|
||||
source_url = download_url
|
||||
|
||||
if source_url and source_url.endswith(".zip"):
|
||||
with urllib.request.urlopen(source_url) as download:
|
||||
with zipfile.ZipFile(io.BytesIO(download.read()), "r") as zf:
|
||||
for member in zf.infolist():
|
||||
zf.extract(member, path="SNI")
|
||||
print(f"Downloaded SNI from {source_url}")
|
||||
|
||||
elif source_url and (source_url.endswith(".tar.xz") or source_url.endswith(".tar.gz")):
|
||||
import tarfile
|
||||
mode = "r:xz" if source_url.endswith(".tar.xz") else "r:gz"
|
||||
with urllib.request.urlopen(source_url) as download:
|
||||
sni_dir = None
|
||||
with tarfile.open(fileobj=io.BytesIO(download.read()), mode=mode) as tf:
|
||||
for member in tf.getmembers():
|
||||
if member.name.startswith("/") or "../" in member.name:
|
||||
raise ValueError(f"Unexpected file '{member.name}' in {source_url}")
|
||||
elif member.isdir() and not sni_dir:
|
||||
sni_dir = member.name
|
||||
elif member.isfile() and not sni_dir or not member.name.startswith(sni_dir):
|
||||
raise ValueError(f"Expected folder before '{member.name}' in {source_url}")
|
||||
elif member.isfile() and sni_dir:
|
||||
tf.extract(member)
|
||||
# sadly SNI is in its own folder on non-windows, so we need to rename
|
||||
shutil.rmtree("SNI", True)
|
||||
os.rename(sni_dir, "SNI")
|
||||
print(f"Downloaded SNI from {source_url}")
|
||||
|
||||
elif source_url:
|
||||
print(f"Don't know how to extract SNI from {source_url}")
|
||||
|
||||
else:
|
||||
print(f"No SNI found for system spec {platform_name} {machine_name}")
|
||||
|
||||
|
||||
signtool: typing.Optional[str]
|
||||
if os.path.exists("X:/pw.txt"):
|
||||
print("Using signtool")
|
||||
with open("X:/pw.txt", encoding="utf-8-sig") as f:
|
||||
pw = f.read()
|
||||
signtool = r'signtool sign /f X:/_SITS_Zertifikat_.pfx /p "' + pw + \
|
||||
r'" /fd sha256 /tr http://timestamp.digicert.com/ '
|
||||
else:
|
||||
signtool = None
|
||||
|
||||
|
||||
build_platform = sysconfig.get_platform()
|
||||
arch_folder = "exe.{platform}-{version}".format(platform=build_platform,
|
||||
version=sysconfig.get_python_version())
|
||||
buildfolder = Path("build", arch_folder)
|
||||
build_arch = build_platform.split('-')[-1] if '-' in build_platform else platform.machine()
|
||||
|
||||
|
||||
# see Launcher.py on how to add scripts to setup.py
|
||||
def resolve_icon(icon_name: str):
|
||||
base_path = icon_paths[icon_name]
|
||||
if is_windows:
|
||||
path, extension = os.path.splitext(base_path)
|
||||
ico_file = path + ".ico"
|
||||
assert os.path.exists(ico_file), f"ico counterpart of {base_path} should exist."
|
||||
return ico_file
|
||||
else:
|
||||
return base_path
|
||||
|
||||
|
||||
exes = [
|
||||
cx_Freeze.Executable(
|
||||
script=f"{c.script_name}.py",
|
||||
target_name="ArchipelagoAHITClient.exe",
|
||||
#target_name=c.frozen_name + (".exe" if is_windows else ""),
|
||||
icon=resolve_icon(c.icon),
|
||||
base="Win32GUI" if is_windows and not c.cli else None
|
||||
) for c in components if c.script_name and c.frozen_name and "AHITClient" in c.script_name
|
||||
]
|
||||
|
||||
#if is_windows:
|
||||
if False:
|
||||
# create a duplicate Launcher for Windows, which has a working stdout/stderr, for debugging and --help
|
||||
c = next(component for component in components if component.script_name == "Launcher")
|
||||
exes.append(cx_Freeze.Executable(
|
||||
script=f"{c.script_name}.py",
|
||||
target_name=f"{c.frozen_name}(DEBUG).exe",
|
||||
icon=resolve_icon(c.icon),
|
||||
))
|
||||
|
||||
extra_data = ["LICENSE", "data", "EnemizerCLI", "SNI"]
|
||||
extra_libs = ["libssl.so", "libcrypto.so"] if is_linux else []
|
||||
|
||||
|
||||
def remove_sprites_from_folder(folder):
|
||||
for file in os.listdir(folder):
|
||||
if file != ".gitignore":
|
||||
os.remove(folder / file)
|
||||
|
||||
|
||||
def _threaded_hash(filepath):
|
||||
hasher = sha3_512()
|
||||
hasher.update(open(filepath, "rb").read())
|
||||
return base64.b85encode(hasher.digest()).decode()
|
||||
|
||||
|
||||
# cx_Freeze's build command runs other commands. Override to accept --yes and store that.
|
||||
class BuildCommand(setuptools.command.build.build):
|
||||
user_options = [
|
||||
('yes', 'y', 'Answer "yes" to all questions.'),
|
||||
]
|
||||
yes: bool
|
||||
last_yes: bool = False # used by sub commands of build
|
||||
|
||||
def initialize_options(self):
|
||||
super().initialize_options()
|
||||
type(self).last_yes = self.yes = False
|
||||
|
||||
def finalize_options(self):
|
||||
super().finalize_options()
|
||||
type(self).last_yes = self.yes
|
||||
|
||||
|
||||
# Override cx_Freeze's build_exe command for pre and post build steps
|
||||
class BuildExeCommand(cx_Freeze.command.build_exe.BuildEXE):
|
||||
user_options = cx_Freeze.command.build_exe.BuildEXE.user_options + [
|
||||
('yes', 'y', 'Answer "yes" to all questions.'),
|
||||
('extra-data=', None, 'Additional files to add.'),
|
||||
]
|
||||
yes: bool
|
||||
extra_data: Iterable # [any] not available in 3.8
|
||||
extra_libs: Iterable # work around broken include_files
|
||||
|
||||
buildfolder: Path
|
||||
libfolder: Path
|
||||
library: Path
|
||||
buildtime: datetime.datetime
|
||||
|
||||
def initialize_options(self):
|
||||
super().initialize_options()
|
||||
self.yes = BuildCommand.last_yes
|
||||
self.extra_data = []
|
||||
self.extra_libs = []
|
||||
|
||||
def finalize_options(self):
|
||||
super().finalize_options()
|
||||
self.buildfolder = self.build_exe
|
||||
self.libfolder = Path(self.buildfolder, "lib")
|
||||
self.library = Path(self.libfolder, "library.zip")
|
||||
|
||||
def installfile(self, path, subpath=None, keep_content: bool = False):
|
||||
folder = self.buildfolder
|
||||
if subpath:
|
||||
folder /= subpath
|
||||
print('copying', path, '->', folder)
|
||||
if path.is_dir():
|
||||
folder /= path.name
|
||||
if folder.is_dir() and not keep_content:
|
||||
shutil.rmtree(folder)
|
||||
shutil.copytree(path, folder, dirs_exist_ok=True)
|
||||
elif path.is_file():
|
||||
shutil.copy(path, folder)
|
||||
else:
|
||||
print('Warning,', path, 'not found')
|
||||
|
||||
def create_manifest(self, create_hashes=False):
|
||||
# Since the setup is now split into components and the manifest is not,
|
||||
# it makes most sense to just remove the hashes for now. Not aware of anyone using them.
|
||||
hashes = {}
|
||||
manifestpath = os.path.join(self.buildfolder, "manifest.json")
|
||||
if create_hashes:
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
pool = ThreadPoolExecutor()
|
||||
for dirpath, dirnames, filenames in os.walk(self.buildfolder):
|
||||
for filename in filenames:
|
||||
path = os.path.join(dirpath, filename)
|
||||
hashes[os.path.relpath(path, start=self.buildfolder)] = pool.submit(_threaded_hash, path)
|
||||
|
||||
import json
|
||||
manifest = {
|
||||
"buildtime": self.buildtime.isoformat(sep=" ", timespec="seconds"),
|
||||
"hashes": {path: hash.result() for path, hash in hashes.items()},
|
||||
"version": version_tuple}
|
||||
|
||||
json.dump(manifest, open(manifestpath, "wt"), indent=4)
|
||||
print("Created Manifest")
|
||||
|
||||
def run(self):
|
||||
# start downloading sni asap
|
||||
sni_thread = threading.Thread(target=download_SNI, name="SNI Downloader")
|
||||
sni_thread.start()
|
||||
|
||||
# pre-build steps
|
||||
print(f"Outputting to: {self.buildfolder}")
|
||||
os.makedirs(self.buildfolder, exist_ok=True)
|
||||
import ModuleUpdate
|
||||
ModuleUpdate.requirements_files.add(os.path.join("WebHostLib", "requirements.txt"))
|
||||
ModuleUpdate.update(yes=self.yes)
|
||||
|
||||
# auto-build cython modules
|
||||
build_ext = self.distribution.get_command_obj("build_ext")
|
||||
build_ext.inplace = False
|
||||
self.run_command("build_ext")
|
||||
# find remains of previous in-place builds, try to delete and warn otherwise
|
||||
for path in build_ext.get_outputs():
|
||||
parts = os.path.split(path)[-1].split(".")
|
||||
pattern = parts[0] + ".*." + parts[-1]
|
||||
for match in Path().glob(pattern):
|
||||
try:
|
||||
match.unlink()
|
||||
print(f"Removed {match}")
|
||||
except Exception as ex:
|
||||
warnings.warn(f"Could not delete old build output: {match}\n"
|
||||
f"{ex}\nPlease close all AP instances and delete manually.")
|
||||
|
||||
# regular cx build
|
||||
self.buildtime = datetime.datetime.utcnow()
|
||||
super().run()
|
||||
|
||||
# manually copy built modules to lib folder. cx_Freeze does not know they exist.
|
||||
for src in build_ext.get_outputs():
|
||||
print(f"copying {src} -> {self.libfolder}")
|
||||
shutil.copy(src, self.libfolder, follow_symlinks=False)
|
||||
|
||||
# need to finish download before copying
|
||||
sni_thread.join()
|
||||
|
||||
# include_files seems to not be done automatically. implement here
|
||||
for src, dst in self.include_files:
|
||||
print(f"copying {src} -> {self.buildfolder / dst}")
|
||||
shutil.copyfile(src, self.buildfolder / dst, follow_symlinks=False)
|
||||
|
||||
# now that include_files is completely broken, run find_libs here
|
||||
for src, dst in find_libs(*self.extra_libs):
|
||||
print(f"copying {src} -> {self.buildfolder / dst}")
|
||||
shutil.copyfile(src, self.buildfolder / dst, follow_symlinks=False)
|
||||
|
||||
# post build steps
|
||||
if is_windows: # kivy_deps is win32 only, linux picks them up automatically
|
||||
from kivy_deps import sdl2, glew
|
||||
for folder in sdl2.dep_bins + glew.dep_bins:
|
||||
shutil.copytree(folder, self.libfolder, dirs_exist_ok=True)
|
||||
print(f"copying {folder} -> {self.libfolder}")
|
||||
|
||||
for data in self.extra_data:
|
||||
self.installfile(Path(data))
|
||||
|
||||
# kivi data files
|
||||
import kivy
|
||||
shutil.copytree(os.path.join(os.path.dirname(kivy.__file__), "data"),
|
||||
self.buildfolder / "data",
|
||||
dirs_exist_ok=True)
|
||||
|
||||
os.makedirs(self.buildfolder / "Players" / "Templates", exist_ok=True)
|
||||
from Options import generate_yaml_templates
|
||||
from worlds.AutoWorld import AutoWorldRegister
|
||||
assert not non_apworlds - set(AutoWorldRegister.world_types), \
|
||||
f"Unknown world {non_apworlds - set(AutoWorldRegister.world_types)} designated for .apworld"
|
||||
folders_to_remove: typing.List[str] = []
|
||||
generate_yaml_templates(self.buildfolder / "Players" / "Templates", False)
|
||||
for worldname, worldtype in AutoWorldRegister.world_types.items():
|
||||
if worldname not in non_apworlds:
|
||||
file_name = os.path.split(os.path.dirname(worldtype.__file__))[1]
|
||||
world_directory = self.libfolder / "worlds" / file_name
|
||||
# this method creates an apworld that cannot be moved to a different OS or minor python version,
|
||||
# which should be ok
|
||||
with zipfile.ZipFile(self.libfolder / "worlds" / (file_name + ".apworld"), "x", zipfile.ZIP_DEFLATED,
|
||||
compresslevel=9) as zf:
|
||||
for path in world_directory.rglob("*.*"):
|
||||
relative_path = os.path.join(*path.parts[path.parts.index("worlds")+1:])
|
||||
zf.write(path, relative_path)
|
||||
folders_to_remove.append(file_name)
|
||||
shutil.rmtree(world_directory)
|
||||
shutil.copyfile("meta.yaml", self.buildfolder / "Players" / "Templates" / "meta.yaml")
|
||||
# TODO: fix LttP options one day
|
||||
shutil.copyfile("playerSettings.yaml", self.buildfolder / "Players" / "Templates" / "A Link to the Past.yaml")
|
||||
try:
|
||||
from maseya import z3pr
|
||||
except ImportError:
|
||||
print("Maseya Palette Shuffle not found, skipping data files.")
|
||||
else:
|
||||
# maseya Palette Shuffle exists and needs its data files
|
||||
print("Maseya Palette Shuffle found, including data files...")
|
||||
file = z3pr.__file__
|
||||
self.installfile(Path(os.path.dirname(file)) / "data", keep_content=True)
|
||||
|
||||
if signtool:
|
||||
for exe in self.distribution.executables:
|
||||
print(f"Signing {exe.target_name}")
|
||||
os.system(signtool + os.path.join(self.buildfolder, exe.target_name))
|
||||
print("Signing SNI")
|
||||
os.system(signtool + os.path.join(self.buildfolder, "SNI", "SNI.exe"))
|
||||
print("Signing OoT Utils")
|
||||
for exe_path in (("Compress", "Compress.exe"), ("Decompress", "Decompress.exe")):
|
||||
os.system(signtool + os.path.join(self.buildfolder, "lib", "worlds", "oot", "data", *exe_path))
|
||||
|
||||
remove_sprites_from_folder(self.buildfolder / "data" / "sprites" / "alttpr")
|
||||
|
||||
self.create_manifest()
|
||||
|
||||
if is_windows:
|
||||
# Inno setup stuff
|
||||
with open("setup.ini", "w") as f:
|
||||
min_supported_windows = "6.2.9200" if sys.version_info > (3, 9) else "6.0.6000"
|
||||
f.write(f"[Data]\nsource_path={self.buildfolder}\nmin_windows={min_supported_windows}\n")
|
||||
with open("installdelete.iss", "w") as f:
|
||||
f.writelines("Type: filesandordirs; Name: \"{app}\\lib\\worlds\\"+world_directory+"\"\n"
|
||||
for world_directory in folders_to_remove)
|
||||
else:
|
||||
# make sure extra programs are executable
|
||||
enemizer_exe = self.buildfolder / 'EnemizerCLI/EnemizerCLI.Core'
|
||||
sni_exe = self.buildfolder / 'SNI/sni'
|
||||
extra_exes = (enemizer_exe, sni_exe)
|
||||
for extra_exe in extra_exes:
|
||||
if extra_exe.is_file():
|
||||
extra_exe.chmod(0o755)
|
||||
|
||||
|
||||
class AppImageCommand(setuptools.Command):
|
||||
description = "build an app image from build output"
|
||||
user_options = [
|
||||
("build-folder=", None, "Folder to convert to AppImage."),
|
||||
("dist-file=", None, "AppImage output file."),
|
||||
("app-dir=", None, "Folder to use for packaging."),
|
||||
("app-icon=", None, "The icon to use for the AppImage."),
|
||||
("app-exec=", None, "The application to run inside the image."),
|
||||
("yes", "y", 'Answer "yes" to all questions.'),
|
||||
]
|
||||
build_folder: typing.Optional[Path]
|
||||
dist_file: typing.Optional[Path]
|
||||
app_dir: typing.Optional[Path]
|
||||
app_name: str
|
||||
app_exec: typing.Optional[Path]
|
||||
app_icon: typing.Optional[Path] # source file
|
||||
app_id: str # lower case name, used for icon and .desktop
|
||||
yes: bool
|
||||
|
||||
def write_desktop(self):
|
||||
assert self.app_dir, "Invalid app_dir"
|
||||
desktop_filename = self.app_dir / f"{self.app_id}.desktop"
|
||||
with open(desktop_filename, 'w', encoding="utf-8") as f:
|
||||
f.write("\n".join((
|
||||
"[Desktop Entry]",
|
||||
f'Name={self.app_name}',
|
||||
f'Exec={self.app_exec}',
|
||||
"Type=Application",
|
||||
"Categories=Game",
|
||||
f'Icon={self.app_id}',
|
||||
''
|
||||
)))
|
||||
desktop_filename.chmod(0o755)
|
||||
|
||||
def write_launcher(self, default_exe: Path):
|
||||
assert self.app_dir, "Invalid app_dir"
|
||||
launcher_filename = self.app_dir / "AppRun"
|
||||
with open(launcher_filename, 'w', encoding="utf-8") as f:
|
||||
f.write(f"""#!/bin/sh
|
||||
exe="{default_exe}"
|
||||
match="${{1#--executable=}}"
|
||||
if [ "${{#match}}" -lt "${{#1}}" ]; then
|
||||
exe="$match"
|
||||
shift
|
||||
elif [ "$1" = "-executable" ] || [ "$1" = "--executable" ]; then
|
||||
exe="$2"
|
||||
shift; shift
|
||||
fi
|
||||
tmp="${{exe#*/}}"
|
||||
if [ ! "${{#tmp}}" -lt "${{#exe}}" ]; then
|
||||
exe="{default_exe.parent}/$exe"
|
||||
fi
|
||||
export LD_LIBRARY_PATH="$LD_LIBRARY_PATH:$APPDIR/{default_exe.parent}/lib"
|
||||
$APPDIR/$exe "$@"
|
||||
""")
|
||||
launcher_filename.chmod(0o755)
|
||||
|
||||
def install_icon(self, src: Path, name: typing.Optional[str] = None, symlink: typing.Optional[Path] = None):
|
||||
assert self.app_dir, "Invalid app_dir"
|
||||
try:
|
||||
from PIL import Image
|
||||
except ModuleNotFoundError:
|
||||
if not self.yes:
|
||||
input("Requirement PIL is not satisfied, press enter to install it")
|
||||
subprocess.call([sys.executable, '-m', 'pip', 'install', 'Pillow', '--upgrade'])
|
||||
from PIL import Image
|
||||
im = Image.open(src)
|
||||
res, _ = im.size
|
||||
|
||||
if not name:
|
||||
name = src.stem
|
||||
ext = src.suffix
|
||||
dest_dir = Path(self.app_dir / f'usr/share/icons/hicolor/{res}x{res}/apps')
|
||||
dest_dir.mkdir(parents=True, exist_ok=True)
|
||||
dest_file = dest_dir / f'{name}{ext}'
|
||||
shutil.copy(src, dest_file)
|
||||
if symlink:
|
||||
symlink.symlink_to(dest_file.relative_to(symlink.parent))
|
||||
|
||||
def initialize_options(self):
|
||||
self.build_folder = None
|
||||
self.app_dir = None
|
||||
self.app_name = self.distribution.metadata.name
|
||||
self.app_icon = self.distribution.executables[0].icon
|
||||
self.app_exec = Path('opt/{app_name}/{exe}'.format(
|
||||
app_name=self.distribution.metadata.name, exe=self.distribution.executables[0].target_name
|
||||
))
|
||||
self.dist_file = Path("dist", "{app_name}_{app_version}_{platform}.AppImage".format(
|
||||
app_name=self.distribution.metadata.name, app_version=self.distribution.metadata.version,
|
||||
platform=sysconfig.get_platform()
|
||||
))
|
||||
self.yes = False
|
||||
|
||||
def finalize_options(self):
|
||||
if not self.app_dir:
|
||||
self.app_dir = self.build_folder.parent / "AppDir"
|
||||
self.app_id = self.app_name.lower()
|
||||
|
||||
def run(self):
|
||||
self.dist_file.parent.mkdir(parents=True, exist_ok=True)
|
||||
if self.app_dir.is_dir():
|
||||
shutil.rmtree(self.app_dir)
|
||||
self.app_dir.mkdir(parents=True)
|
||||
opt_dir = self.app_dir / "opt" / self.distribution.metadata.name
|
||||
shutil.copytree(self.build_folder, opt_dir)
|
||||
root_icon = self.app_dir / f'{self.app_id}{self.app_icon.suffix}'
|
||||
self.install_icon(self.app_icon, self.app_id, symlink=root_icon)
|
||||
shutil.copy(root_icon, self.app_dir / '.DirIcon')
|
||||
self.write_desktop()
|
||||
self.write_launcher(self.app_exec)
|
||||
print(f'{self.app_dir} -> {self.dist_file}')
|
||||
subprocess.call(f'ARCH={build_arch} ./appimagetool -n "{self.app_dir}" "{self.dist_file}"', shell=True)
|
||||
|
||||
|
||||
def find_libs(*args: str) -> typing.Sequence[typing.Tuple[str, str]]:
|
||||
"""Try to find system libraries to be included."""
|
||||
if not args:
|
||||
return []
|
||||
|
||||
arch = build_arch.replace('_', '-')
|
||||
libc = 'libc6' # we currently don't support musl
|
||||
|
||||
def parse(line):
|
||||
lib, path = line.strip().split(' => ')
|
||||
lib, typ = lib.split(' ', 1)
|
||||
for test_arch in ('x86-64', 'i386', 'aarch64'):
|
||||
if test_arch in typ:
|
||||
lib_arch = test_arch
|
||||
break
|
||||
else:
|
||||
lib_arch = ''
|
||||
for test_libc in ('libc6',):
|
||||
if test_libc in typ:
|
||||
lib_libc = test_libc
|
||||
break
|
||||
else:
|
||||
lib_libc = ''
|
||||
return (lib, lib_arch, lib_libc), path
|
||||
|
||||
if not hasattr(find_libs, "cache"):
|
||||
ldconfig = shutil.which("ldconfig")
|
||||
assert ldconfig, "Make sure ldconfig is in PATH"
|
||||
data = subprocess.run([ldconfig, "-p"], capture_output=True, text=True).stdout.split("\n")[1:]
|
||||
find_libs.cache = { # type: ignore [attr-defined]
|
||||
k: v for k, v in (parse(line) for line in data if "=>" in line)
|
||||
}
|
||||
|
||||
def find_lib(lib, arch, libc):
|
||||
for k, v in find_libs.cache.items():
|
||||
if k == (lib, arch, libc):
|
||||
return v
|
||||
for k, v, in find_libs.cache.items():
|
||||
if k[0].startswith(lib) and k[1] == arch and k[2] == libc:
|
||||
return v
|
||||
return None
|
||||
|
||||
res = []
|
||||
for arg in args:
|
||||
# try exact match, empty libc, empty arch, empty arch and libc
|
||||
file = find_lib(arg, arch, libc)
|
||||
file = file or find_lib(arg, arch, '')
|
||||
file = file or find_lib(arg, '', libc)
|
||||
file = file or find_lib(arg, '', '')
|
||||
# resolve symlinks
|
||||
for n in range(0, 5):
|
||||
res.append((file, os.path.join('lib', os.path.basename(file))))
|
||||
if not os.path.islink(file):
|
||||
break
|
||||
dirname = os.path.dirname(file)
|
||||
file = os.readlink(file)
|
||||
if not os.path.isabs(file):
|
||||
file = os.path.join(dirname, file)
|
||||
return res
|
||||
|
||||
|
||||
cx_Freeze.setup(
|
||||
name="Archipelago",
|
||||
version=f"{version_tuple.major}.{version_tuple.minor}.{version_tuple.build}",
|
||||
description="Archipelago",
|
||||
executables=exes,
|
||||
ext_modules=cythonize("_speedups.pyx"),
|
||||
options={
|
||||
"build_exe": {
|
||||
"packages": ["worlds", "kivy", "cymem", "websockets"],
|
||||
"includes": [],
|
||||
"excludes": ["numpy", "Cython", "PySide2", "PIL",
|
||||
"pandas"],
|
||||
"zip_include_packages": ["*"],
|
||||
"zip_exclude_packages": ["worlds", "sc2"],
|
||||
"include_files": [], # broken in cx 6.14.0, we use more special sauce now
|
||||
"include_msvcr": False,
|
||||
"replace_paths": ["*."],
|
||||
"optimize": 1,
|
||||
"build_exe": buildfolder,
|
||||
"extra_data": extra_data,
|
||||
"extra_libs": extra_libs,
|
||||
"bin_includes": ["libffi.so", "libcrypt.so"] if is_linux else []
|
||||
},
|
||||
"bdist_appimage": {
|
||||
"build_folder": buildfolder,
|
||||
},
|
||||
},
|
||||
# override commands to get custom stuff in
|
||||
cmdclass={
|
||||
"build": BuildCommand,
|
||||
"build_exe": BuildExeCommand,
|
||||
"bdist_appimage": AppImageCommand,
|
||||
},
|
||||
)
|
||||
@@ -1,6 +1,6 @@
|
||||
from worlds.AutoWorld import World, CollectionState
|
||||
from .Rules import can_use_hat, can_use_hookshot, can_hit, zipline_logic, get_difficulty
|
||||
from .Types import HatType, Difficulty, HatInTimeLocation, HatInTimeItem, LocData, HatDLC
|
||||
from .Rules import can_use_hat, can_use_hookshot, can_hit, zipline_logic, get_difficulty, has_paintings
|
||||
from .Types import HatType, Difficulty, HatInTimeLocation, HatInTimeItem, LocData
|
||||
from .DeathWishLocations import dw_prereqs, dw_candles
|
||||
from BaseClasses import Entrance, Location, ItemClassification
|
||||
from worlds.generic.Rules import add_rule, set_rule
|
||||
@@ -169,8 +169,7 @@ def set_dw_rules(world: World):
|
||||
add_rule(loc, lambda state: state.has("Umbrella", world.player))
|
||||
|
||||
if data.paintings > 0 and world.multiworld.ShuffleSubconPaintings[world.player].value > 0:
|
||||
add_rule(loc, lambda state, paintings=data.paintings: state.has("Progressive Painting Unlock",
|
||||
world.player, paintings))
|
||||
add_rule(loc, lambda state, paintings=data.paintings: has_paintings(state, world, paintings))
|
||||
|
||||
if data.hit_requirement > 0:
|
||||
if data.hit_requirement == 1:
|
||||
@@ -303,10 +302,17 @@ def set_candle_dw_rules(name: str, world: World):
|
||||
and state.has("Train Rush (Zero Jumps)", world.player) and can_use_hat(state, world, HatType.ICE))
|
||||
|
||||
# No Ice Hat/painting required in Expert for Toilet Zero Jumps
|
||||
if get_difficulty(world) >= Difficulty.EXPERT:
|
||||
# This painting wall can only be skipped via cherry hover.
|
||||
if get_difficulty(world) < Difficulty.EXPERT or world.multiworld.NoPaintingSkips[world.player].value == 1:
|
||||
set_rule(world.multiworld.get_location("Toilet of Doom (Zero Jumps)", world.player),
|
||||
lambda state: can_use_hookshot(state, world)
|
||||
and can_hit(state, world))
|
||||
lambda state: can_use_hookshot(state, world) and can_hit(state, world)
|
||||
and has_paintings(state, world, 1, False))
|
||||
else:
|
||||
set_rule(world.multiworld.get_location("Toilet of Doom (Zero Jumps)", world.player),
|
||||
lambda state: can_use_hookshot(state, world) and can_hit(state, world))
|
||||
|
||||
set_rule(world.multiworld.get_location("Contractual Obligations (Zero Jumps)", world.player),
|
||||
lambda state: has_paintings(state, world, 1, False))
|
||||
|
||||
elif name == "Snatcher's Hit List":
|
||||
add_rule(main_objective, lambda state: state.has("Mafia Goon", world.player))
|
||||
|
||||
@@ -230,8 +230,8 @@ ahit_items = {
|
||||
|
||||
# DLC1 items
|
||||
"Relic (Cake Stand)": ItemData(2000300018, ItemClassification.progression, HatDLC.dlc1),
|
||||
"Relic (Cake)": ItemData(2000300019, ItemClassification.progression, HatDLC.dlc1),
|
||||
"Relic (Cake Slice)": ItemData(2000300020, ItemClassification.progression, HatDLC.dlc1),
|
||||
"Relic (Chocolate Cake)": ItemData(2000300019, ItemClassification.progression, HatDLC.dlc1),
|
||||
"Relic (Chocolate Cake Slice)": ItemData(2000300020, ItemClassification.progression, HatDLC.dlc1),
|
||||
"Relic (Shortcake)": ItemData(2000300021, ItemClassification.progression, HatDLC.dlc1),
|
||||
|
||||
# DLC2 items
|
||||
|
||||
@@ -211,6 +211,12 @@ class ShuffleSubconPaintings(Toggle):
|
||||
default = 0
|
||||
|
||||
|
||||
class NoPaintingSkips(Toggle):
|
||||
"""If enabled, prevent Subcon fire wall skips from being in logic on higher difficulty settings."""
|
||||
display_name = "No Subcon Fire Wall Skips"
|
||||
default = 0
|
||||
|
||||
|
||||
class StartingChapter(Choice):
|
||||
"""Determines which chapter you will be guaranteed to be able to enter at the beginning of the game."""
|
||||
display_name = "Starting Chapter"
|
||||
@@ -606,6 +612,7 @@ ahit_options: typing.Dict[str, type(Option)] = {
|
||||
"ShuffleStorybookPages": ShuffleStorybookPages,
|
||||
"ShuffleActContracts": ShuffleActContracts,
|
||||
"ShuffleSubconPaintings": ShuffleSubconPaintings,
|
||||
"NoPaintingSkips": NoPaintingSkips,
|
||||
"StartingChapter": StartingChapter,
|
||||
"CTRLogic": CTRLogic,
|
||||
|
||||
@@ -677,6 +684,7 @@ slot_data_options: typing.Dict[str, type(Option)] = {
|
||||
"ShuffleStorybookPages": ShuffleStorybookPages,
|
||||
"ShuffleActContracts": ShuffleActContracts,
|
||||
"ShuffleSubconPaintings": ShuffleSubconPaintings,
|
||||
"NoPaintingSkips": NoPaintingSkips,
|
||||
"HatItems": HatItems,
|
||||
|
||||
"EnableDLC1": EnableDLC1,
|
||||
|
||||
@@ -62,25 +62,16 @@ def get_difficulty(world: World) -> Difficulty:
|
||||
return Difficulty(world.multiworld.LogicDifficulty[world.player].value)
|
||||
|
||||
|
||||
def has_paintings(state: CollectionState, world: World, count: int, surf: bool = True) -> bool:
|
||||
def has_paintings(state: CollectionState, world: World, count: int, allow_skip: bool=True) -> bool:
|
||||
if not painting_logic(world):
|
||||
return True
|
||||
|
||||
# Cherry Hover
|
||||
if get_difficulty(world) >= Difficulty.EXPERT:
|
||||
return True
|
||||
|
||||
# All paintings can be skipped with No Bonk, very easily, if the player knows
|
||||
if surf and get_difficulty(world) >= Difficulty.MODERATE and can_surf(state, world):
|
||||
return True
|
||||
|
||||
paintings: int = state.count("Progressive Painting Unlock", world.player)
|
||||
if surf and get_difficulty(world) >= Difficulty.MODERATE:
|
||||
# Green+Yellow paintings can also be skipped easily
|
||||
if count == 1 or paintings >= 1 and count == 3:
|
||||
if world.multiworld.NoPaintingSkips[world.player].value == 0 and allow_skip:
|
||||
# In Moderate there is a very easy trick to skip all the walls, except for the one guarding the boss arena
|
||||
if get_difficulty(world) >= Difficulty.MODERATE:
|
||||
return True
|
||||
|
||||
return paintings >= count
|
||||
return state.count("Progressive Painting Unlock", world.player) >= count
|
||||
|
||||
|
||||
def zipline_logic(world: World) -> bool:
|
||||
@@ -281,10 +272,7 @@ def set_rules(world: World):
|
||||
add_rule(location, lambda state: state.has("Umbrella", world.player))
|
||||
|
||||
if data.paintings > 0 and world.multiworld.ShuffleSubconPaintings[world.player].value > 0:
|
||||
if "Toilet of Doom" not in key:
|
||||
add_rule(location, lambda state, paintings=data.paintings: has_paintings(state, world, paintings))
|
||||
else:
|
||||
add_rule(location, lambda state, paintings=data.paintings: has_paintings(state, world, paintings, False))
|
||||
add_rule(location, lambda state, paintings=data.paintings: has_paintings(state, world, paintings))
|
||||
|
||||
if data.hit_requirement > 0:
|
||||
if data.hit_requirement == 1:
|
||||
@@ -417,13 +405,9 @@ def set_moderate_rules(world: World):
|
||||
add_rule(world.multiworld.get_location("Mafia Town - Clock Tower Chest", world.player), lambda state: True)
|
||||
add_rule(world.multiworld.get_location("Mafia Town - Top of Ruined Tower", world.player), lambda state: True)
|
||||
|
||||
# Moderate: hitting the bell is not required to enter Subcon Well, however hookshot is still expected to clear
|
||||
set_rule(world.multiworld.get_location("Subcon Well - Hookshot Badge Chest", world.player),
|
||||
lambda state: has_paintings(state, world, 1))
|
||||
set_rule(world.multiworld.get_location("Subcon Well - Above Chest", world.player),
|
||||
lambda state: has_paintings(state, world, 1))
|
||||
set_rule(world.multiworld.get_location("Subcon Well - Mushroom", world.player),
|
||||
lambda state: has_paintings(state, world, 1))
|
||||
# Moderate: enter and clear The Subcon Well without Hookshot and without hitting the bell
|
||||
for loc in world.multiworld.get_region("The Subcon Well", world.player).locations:
|
||||
set_rule(loc, lambda state: has_paintings(state, world, 1))
|
||||
|
||||
# Moderate: Vanessa Manor with nothing
|
||||
for loc in world.multiworld.get_region("Queen Vanessa's Manor", world.player).locations:
|
||||
@@ -484,17 +468,17 @@ def set_hard_rules(world: World):
|
||||
lambda state: can_use_hat(state, world, HatType.SPRINT)
|
||||
and state.has("Scooter Badge", world.player), "or")
|
||||
|
||||
# No Dweller Mask required
|
||||
set_rule(world.multiworld.get_location("Subcon Forest - Dweller Floating Rocks", world.player),
|
||||
lambda state: has_paintings(state, world, 3))
|
||||
|
||||
# Cherry bridge over boss arena gap (painting still expected)
|
||||
set_rule(world.multiworld.get_location("Subcon Forest - Boss Arena Chest", world.player),
|
||||
lambda state: has_paintings(state, world, 1, False))
|
||||
lambda state: has_paintings(state, world, 1, False) or state.has("YCHE Access", world.player))
|
||||
|
||||
# SDJ
|
||||
add_rule(world.multiworld.get_location("Subcon Forest - Long Tree Climb Chest", world.player),
|
||||
lambda state: can_sdj(state, world)
|
||||
and has_paintings(state, world, 2), "or")
|
||||
lambda state: can_sdj(state, world) and has_paintings(state, world, 2), "or")
|
||||
|
||||
add_rule(world.multiworld.get_location("Subcon Forest - Dweller Platforming Tree B", world.player),
|
||||
lambda state: has_paintings(state, world, 3) and can_sdj(state, world), "or")
|
||||
@@ -559,21 +543,29 @@ def set_expert_rules(world: World):
|
||||
set_rule(world.multiworld.get_location("Act Completion (Time Rift - The Twilight Bell)", world.player),
|
||||
lambda state: True)
|
||||
|
||||
# Expert: enter and clear The Subcon Well with nothing
|
||||
for loc in world.multiworld.get_region("The Subcon Well", world.player).locations:
|
||||
set_rule(loc, lambda state: True)
|
||||
|
||||
# Expert: Cherry Hovering
|
||||
connect_regions(world.multiworld.get_region("Your Contract has Expired", world.player),
|
||||
world.multiworld.get_region("Subcon Forest Area", world.player),
|
||||
"Subcon Forest Entrance YCHE", world.player)
|
||||
entrance = connect_regions(world.multiworld.get_region("Your Contract has Expired", world.player),
|
||||
world.multiworld.get_region("Subcon Forest Area", world.player),
|
||||
"Subcon Forest Entrance YCHE", world.player)
|
||||
|
||||
set_rule(world.multiworld.get_location("Subcon Forest - Boss Arena Chest", world.player), lambda state: True)
|
||||
set_rule(world.multiworld.get_location("Subcon Forest - Noose Treehouse", world.player), lambda state: True)
|
||||
set_rule(world.multiworld.get_location("Subcon Forest - Long Tree Climb Chest", world.player), lambda state: True)
|
||||
set_rule(world.multiworld.get_location("Subcon Forest - Noose Treehouse", world.player), lambda state: True)
|
||||
set_rule(world.multiworld.get_location("Subcon Forest - Dweller Platforming Tree B", world.player), lambda state: True)
|
||||
set_rule(world.multiworld.get_location("Subcon Forest - Tall Tree Hookshot Swing", world.player), lambda state: True)
|
||||
if world.multiworld.NoPaintingSkips[world.player].value > 0:
|
||||
add_rule(entrance, lambda state: has_paintings(state, world, 1))
|
||||
|
||||
set_rule(world.multiworld.get_location("Act Completion (Toilet of Doom)", world.player),
|
||||
lambda state: can_use_hookshot(state, world) and can_hit(state, world)
|
||||
and has_paintings(state, world, 1, True))
|
||||
|
||||
# Set painting rules only. Skipping paintings is determined in has_paintings
|
||||
set_rule(world.multiworld.get_location("Subcon Forest - Boss Arena Chest", world.player),
|
||||
lambda state: has_paintings(state, world, 1, True))
|
||||
set_rule(world.multiworld.get_location("Subcon Forest - Noose Treehouse", world.player),
|
||||
lambda state: has_paintings(state, world, 2, True))
|
||||
set_rule(world.multiworld.get_location("Subcon Forest - Long Tree Climb Chest", world.player),
|
||||
lambda state: has_paintings(state, world, 2, True))
|
||||
set_rule(world.multiworld.get_location("Subcon Forest - Dweller Platforming Tree B", world.player),
|
||||
lambda state: has_paintings(state, world, 3, True))
|
||||
set_rule(world.multiworld.get_location("Subcon Forest - Tall Tree Hookshot Swing", world.player),
|
||||
lambda state: has_paintings(state, world, 3, True))
|
||||
|
||||
# You can cherry hover to Snatcher's post-fight cutscene, which completes the level without having to fight him
|
||||
connect_regions(world.multiworld.get_region("Subcon Forest Area", world.player),
|
||||
@@ -648,15 +640,20 @@ def set_mafia_town_rules(world: World):
|
||||
|
||||
|
||||
def set_subcon_rules(world: World):
|
||||
set_rule(world.multiworld.get_location("Subcon Forest - Boss Arena Chest", world.player),
|
||||
lambda state: state.has("TOD Access", world.player) and can_use_hookshot(state, world)
|
||||
and (not painting_logic(world) or has_paintings(state, world, 1))
|
||||
or state.has("YCHE Access", world.player))
|
||||
|
||||
set_rule(world.multiworld.get_location("Act Completion (Time Rift - Village)", world.player),
|
||||
lambda state: can_use_hat(state, world, HatType.BREWING) or state.has("Umbrella", world.player)
|
||||
or can_use_hat(state, world, HatType.DWELLER))
|
||||
|
||||
# You can't skip over the boss arena wall without cherry hover, so these two need to be set this way
|
||||
set_rule(world.multiworld.get_location("Subcon Forest - Boss Arena Chest", world.player),
|
||||
lambda state: state.has("TOD Access", world.player) and can_use_hookshot(state, world)
|
||||
and has_paintings(state, world, 1, False) or state.has("YCHE Access", world.player))
|
||||
|
||||
# The painting wall can't be skipped without cherry hover, which is Expert
|
||||
set_rule(world.multiworld.get_location("Act Completion (Toilet of Doom)", world.player),
|
||||
lambda state: can_use_hookshot(state, world) and can_hit(state, world)
|
||||
and has_paintings(state, world, 1, False))
|
||||
|
||||
add_rule(world.multiworld.get_entrance("Subcon Forest - Act 2", world.player),
|
||||
lambda state: state.has("Snatcher's Contract - The Subcon Well", world.player))
|
||||
|
||||
@@ -671,7 +668,7 @@ def set_subcon_rules(world: World):
|
||||
|
||||
if painting_logic(world):
|
||||
add_rule(world.multiworld.get_location("Act Completion (Contractual Obligations)", world.player),
|
||||
lambda state: state.has("Progressive Painting Unlock", world.player))
|
||||
lambda state: has_paintings(state, world, 1, False))
|
||||
|
||||
for key in contract_locations:
|
||||
if key == "Snatcher's Contract - The Subcon Well":
|
||||
|
||||
@@ -9,7 +9,9 @@ from .DeathWishLocations import create_dw_regions, dw_classes, death_wishes
|
||||
from .DeathWishRules import set_dw_rules, create_enemy_events
|
||||
from worlds.AutoWorld import World, WebWorld
|
||||
from typing import List, Dict, TextIO
|
||||
from worlds.LauncherComponents import Component, components
|
||||
from worlds.LauncherComponents import Component, components, icon_paths
|
||||
from multiprocessing import Process
|
||||
from Utils import local_path
|
||||
|
||||
hat_craft_order: Dict[int, List[HatType]] = {}
|
||||
hat_yarn_costs: Dict[int, Dict[HatType, int]] = {}
|
||||
@@ -20,7 +22,14 @@ dw_shuffle: Dict[int, List[str]] = {}
|
||||
nyakuza_thug_items: Dict[int, Dict[str, int]] = {}
|
||||
badge_seller_count: Dict[int, int] = {}
|
||||
|
||||
components.append(Component("A Hat in Time Client", "AHITClient"))
|
||||
components.append(Component("A Hat in Time Client", "AHITClient", icon='yatta'))
|
||||
icon_paths['yatta'] = local_path('data', 'yatta.png')
|
||||
|
||||
|
||||
def run_client():
|
||||
from AHITClient import main
|
||||
p = Process(target=main)
|
||||
p.start()
|
||||
|
||||
|
||||
class AWebInTime(WebWorld):
|
||||
@@ -170,7 +179,8 @@ class HatInTimeWorld(World):
|
||||
"Chapter6Cost": chapter_timepiece_costs[self.player][ChapterIndex.CRUISE],
|
||||
"Chapter7Cost": chapter_timepiece_costs[self.player][ChapterIndex.METRO],
|
||||
"BadgeSellerItemCount": badge_seller_count[self.player],
|
||||
"SeedNumber": str(self.multiworld.seed)} # For shop prices
|
||||
"SeedNumber": str(self.multiworld.seed), # For shop prices
|
||||
"SeedName": self.multiworld.seed_name}
|
||||
|
||||
if self.multiworld.HatItems[self.player].value == 0:
|
||||
slot_data.setdefault("SprintYarnCost", hat_yarn_costs[self.player][HatType.SPRINT])
|
||||
|
||||
Reference in New Issue
Block a user