diff --git a/.gitignore b/.gitignore index 1d7b3795..26885103 100644 --- a/.gitignore +++ b/.gitignore @@ -152,3 +152,7 @@ dmypy.json cython_debug/ Archipelago.zip + +#minecraft server stuff +jdk*/ +minecraft*/ \ No newline at end of file diff --git a/BaseClasses.py b/BaseClasses.py index 01124129..0c90f37c 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -586,7 +586,7 @@ class CollectionState(object): return any(shop.region.player == player and shop.has(item) and shop.region.can_reach(self) for shop in self.world.shops) - def item_count(self, item, player: int) -> int: + def item_count(self, item: str, player: int) -> int: return self.prog_items[item, player] def has_triforce_pieces(self, count: int, player: int) -> bool: @@ -713,23 +713,23 @@ class CollectionState(object): def has_turtle_rock_medallion(self, player: int) -> bool: return self.has(self.world.required_medallions[player][1], player) - def can_boots_clip_lw(self, player): + def can_boots_clip_lw(self, player: int): if self.world.mode[player] == 'inverted': return self.has('Pegasus Boots', player) and self.has('Moon Pearl', player) return self.has('Pegasus Boots', player) - def can_boots_clip_dw(self, player): + def can_boots_clip_dw(self, player: int): if self.world.mode[player] != 'inverted': return self.has('Pegasus Boots', player) and self.has('Moon Pearl', player) return self.has('Pegasus Boots', player) - def can_get_glitched_speed_lw(self, player): + def can_get_glitched_speed_lw(self, player: int): rules = [self.has('Pegasus Boots', player), any([self.has('Hookshot', player), self.has_sword(player)])] if self.world.mode[player] == 'inverted': rules.append(self.has('Moon Pearl', player)) return all(rules) - def can_superbunny_mirror_with_sword(self, player): + def can_superbunny_mirror_with_sword(self, player: int): return self.has('Magic Mirror', player) and self.has_sword(player) def can_get_glitched_speed_dw(self, player: int): @@ -758,7 +758,7 @@ class CollectionState(object): return changed - def remove(self, item): + def remove(self, item: Item): changed = self.world.worlds[item.player].remove(self, item) if changed: # invalidate caches, nothing can be trusted anymore now @@ -776,14 +776,14 @@ class RegionType(int, Enum): Dungeon = 4 @property - def is_indoors(self): + def is_indoors(self) -> bool: """Shorthand for checking if Cave or Dungeon""" return self in (RegionType.Cave, RegionType.Dungeon) class Region(object): - def __init__(self, name: str, type, hint, player: int, world: Optional[MultiWorld] = None): + def __init__(self, name: str, type: str, hint, player: int, world: Optional[MultiWorld] = None): self.name = name self.type = type self.entrances = [] @@ -798,12 +798,12 @@ class Region(object): self.hint_text = hint self.player = player - def can_reach(self, state: CollectionState): + def can_reach(self, state: CollectionState) -> bool: if state.stale[self.player]: state.update_reachable_regions(self.player) return self in state.reachable_regions[self.player] - def can_reach_private(self, state: CollectionState): + def can_reach_private(self, state: CollectionState) -> bool: for entrance in self.entrances: if entrance.can_reach(state): if not self in state.path: @@ -831,7 +831,7 @@ class Entrance(object): self.player = player self.hide_path = False - def can_reach(self, state): + def can_reach(self, state: CollectionState) -> bool: if self.parent_region.can_reach(state) and self.access_rule(state): if not self.hide_path and not self in state.path: state.path[self] = (self.name, state.path.get(self.parent_region, (self.parent_region.name, None))) @@ -839,7 +839,7 @@ class Entrance(object): return False - def connect(self, region, addresses=None, target=None): + def connect(self, region: Region, addresses=None, target = None): self.connected_region = region self.target = target self.addresses = addresses @@ -865,11 +865,11 @@ class Dungeon(object): self.world = None @property - def boss(self): + def boss(self) -> Optional[Boss]: return self.bosses.get(None, None) @boss.setter - def boss(self, value): + def boss(self, value: Optional[Boss]): self.bosses[None] = value @property @@ -896,7 +896,7 @@ class Dungeon(object): class Boss(): - def __init__(self, name, enemizer_name, defeat_rule, player: int): + def __init__(self, name: str, enemizer_name: str, defeat_rule, player: int): self.name = name self.enemizer_name = enemizer_name self.defeat_rule = defeat_rule diff --git a/CommonClient.py b/CommonClient.py index ea8170b1..6f9d28d7 100644 --- a/CommonClient.py +++ b/CommonClient.py @@ -82,16 +82,19 @@ class ClientCommandProcessor(CommandProcessor): return True def _cmd_items(self): + """List all item names for the currently running game.""" self.output(f"Item Names for {self.ctx.game}") for item_name in AutoWorldRegister.world_types[self.ctx.game].item_name_to_id: self.output(item_name) def _cmd_locations(self): + """List all location names for the currently running game.""" self.output(f"Location Names for {self.ctx.game}") for location_name in AutoWorldRegister.world_types[self.ctx.game].location_name_to_id: self.output(location_name) def _cmd_ready(self): + """Send ready status to server.""" self.ctx.ready = not self.ctx.ready if self.ctx.ready: state = ClientStatus.CLIENT_READY diff --git a/FF1Client.py b/FF1Client.py index 3666d950..90d2b484 100644 --- a/FF1Client.py +++ b/FF1Client.py @@ -11,9 +11,9 @@ from CommonClient import CommonContext, server_loop, gui_enabled, console_loop, SYSTEM_MESSAGE_ID = 0 -CONNECTION_TIMING_OUT_STATUS = "Connection timing out. Please restart your emulator then restart ff1_connector.lua" -CONNECTION_REFUSED_STATUS = "Connection Refused. Please start your emulator make sure ff1_connector.lua is running" -CONNECTION_RESET_STATUS = "Connection was reset. Please restart your emulator then restart ff1_connector.lua" +CONNECTION_TIMING_OUT_STATUS = "Connection timing out. Please restart your emulator, then restart ff1_connector.lua" +CONNECTION_REFUSED_STATUS = "Connection Refused. Please start your emulator and make sure ff1_connector.lua is running" +CONNECTION_RESET_STATUS = "Connection was reset. Please restart your emulator, then restart ff1_connector.lua" CONNECTION_TENTATIVE_STATUS = "Initial Connection Made" CONNECTION_CONNECTED_STATUS = "Connected" CONNECTION_INITIAL_STATUS = "Connection has not been initiated" @@ -219,8 +219,8 @@ if __name__ == '__main__': ctx.server_task = asyncio.create_task(server_loop(ctx), name="ServerLoop") if gui_enabled: input_task = None - from kvui import TextManager - ctx.ui = TextManager(ctx) + from kvui import FF1Manager + ctx.ui = FF1Manager(ctx) ui_task = asyncio.create_task(ctx.ui.async_run(), name="UI") else: input_task = asyncio.create_task(console_loop(ctx), name="Input") diff --git a/LttPAdjuster.py b/LttPAdjuster.py index 77732059..148fa55f 100644 --- a/LttPAdjuster.py +++ b/LttPAdjuster.py @@ -20,7 +20,7 @@ from urllib.parse import urlparse from urllib.request import urlopen from worlds.alttp.Rom import Sprite, LocalRom, apply_rom_settings, get_base_rom_bytes -from Utils import output_path, local_path, open_file +from Utils import output_path, local_path, open_file, get_cert_none_ssl_context, persistent_store class AdjusterWorld(object): @@ -102,14 +102,15 @@ def main(): parser.add_argument('--update_sprites', action='store_true', help='Update Sprite Database, then exit.') args = parser.parse_args() args.music = not args.disablemusic - if args.update_sprites: - run_sprite_update() - sys.exit() # set up logger loglevel = {'error': logging.ERROR, 'info': logging.INFO, 'warning': logging.WARNING, 'debug': logging.DEBUG}[ args.loglevel] logging.basicConfig(format='%(message)s', level=loglevel) + if args.update_sprites: + run_sprite_update() + sys.exit() + if not os.path.isfile(args.rom): adjustGUI() else: @@ -118,7 +119,6 @@ def main(): sys.exit(1) args, path = adjust(args=args) - from Utils import persistent_store if isinstance(args.sprite, Sprite): args.sprite = args.sprite.name persistent_store("adjuster", "last_settings_3", args) @@ -224,7 +224,6 @@ def adjustGUI(): messagebox.showerror(title="Error while adjusting Rom", message=str(e)) else: messagebox.showinfo(title="Success", message=f"Rom patched successfully to {path}") - from Utils import persistent_store if isinstance(guiargs.sprite, Sprite): guiargs.sprite = guiargs.sprite.name persistent_store("adjuster", "last_settings_3", guiargs) @@ -241,12 +240,16 @@ def adjustGUI(): def run_sprite_update(): import threading done = threading.Event() - top = Tk() - top.withdraw() - BackgroundTaskProgress(top, update_sprites, "Updating Sprites", lambda succesful, resultmessage: done.set()) + try: + top = Tk() + except: + task = BackgroundTaskProgressNullWindow(update_sprites, lambda successful, resultmessage: done.set()) + else: + top.withdraw() + task = BackgroundTaskProgress(top, update_sprites, "Updating Sprites", lambda succesful, resultmessage: done.set()) while not done.isSet(): - top.update() - print("Done updating sprites") + task.do_events() + logging.info("Done updating sprites") def update_sprites(task, on_finish=None): @@ -254,7 +257,7 @@ def update_sprites(task, on_finish=None): successful = True sprite_dir = local_path("data", "sprites", "alttpr") os.makedirs(sprite_dir, exist_ok=True) - + ctx = get_cert_none_ssl_context() def finished(): task.close_window() if on_finish: @@ -262,7 +265,7 @@ def update_sprites(task, on_finish=None): try: task.update_status("Downloading alttpr sprites list") - with urlopen('https://alttpr.com/sprites') as response: + with urlopen('https://alttpr.com/sprites', context=ctx) as response: sprites_arr = json.loads(response.read().decode("utf-8")) except Exception as e: resultmessage = "Error getting list of alttpr sprites. Sprites not updated.\n\n%s: %s" % (type(e).__name__, e) @@ -289,7 +292,7 @@ def update_sprites(task, on_finish=None): def dl(sprite_url, filename): target = os.path.join(sprite_dir, filename) - with urlopen(sprite_url) as response, open(target, 'wb') as out: + with urlopen(sprite_url, context=ctx) as response, open(target, 'wb') as out: shutil.copyfileobj(response, out) def rem(sprite): @@ -400,12 +403,39 @@ class BackgroundTaskProgress(BackgroundTask): def update_status(self, text): self.queue_event(lambda: self.label_var.set(text)) + def do_events(self): + self.parent.update() + # only call this in an event callback def close_window(self): self.stop() self.window.destroy() +class BackgroundTaskProgressNullWindow(BackgroundTask): + def __init__(self, code_to_run, *args): + super().__init__(None, code_to_run, *args) + + def process_queue(self): + try: + while True: + if not self.running: + return + event = self.queue.get_nowait() + event() + except queue.Empty: + pass + + def do_events(self): + self.process_queue() + + def update_status(self, text): + self.queue_event(lambda: logging.info(text)) + + def close_window(self): + self.stop() + + def get_rom_frame(parent=None): romFrame = Frame(parent) baseRomLabel = Label(romFrame, text='LttP Base Rom: ') diff --git a/Main.py b/Main.py index 3fed538d..5d7ea8e8 100644 --- a/Main.py +++ b/Main.py @@ -321,8 +321,7 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No logger.warning("Location Accessibility requirements not fulfilled.") # retrieve exceptions via .result() if they occured. - if multidata_task: - multidata_task.result() + multidata_task.result() for i, future in enumerate(concurrent.futures.as_completed(output_file_futures), start=1): if i % 10 == 0 or i == len(output_file_futures): logger.info(f'Generating output files ({i}/{len(output_file_futures)}).') diff --git a/MinecraftClient.py b/MinecraftClient.py index 509c70b0..cc4e0820 100644 --- a/MinecraftClient.py +++ b/MinecraftClient.py @@ -15,6 +15,7 @@ atexit.register(input, "Press enter to exit.") # 1 or more digits followed by m or g, then optional b max_heap_re = re.compile(r"^\d+[mMgG][bB]?$") +forge_version = "1.17.1-37.1.1" def prompt_yes_no(prompt): @@ -30,25 +31,14 @@ def prompt_yes_no(prompt): print('Please respond with "y" or "n".') -# Find Forge jar file; raise error if not found -def find_forge_jar(forge_dir): - for entry in os.scandir(forge_dir): - if ".jar" in entry.name and "forge" in entry.name: - logging.info(f"Found forge .jar: {entry.name}") - return entry.name - raise FileNotFoundError(f"Could not find forge .jar in {forge_dir}.") - - # Create mods folder if needed; find AP randomizer jar; return None if not found. def find_ap_randomizer_jar(forge_dir): mods_dir = os.path.join(forge_dir, 'mods') if os.path.isdir(mods_dir): - ap_mod_re = re.compile(r"^aprandomizer-[\d\.]+\.jar$") for entry in os.scandir(mods_dir): - match = ap_mod_re.match(entry.name) - if match: - logging.info(f"Found AP randomizer mod: {match.group()}") - return match.group() + if entry.name.startswith("aprandomizer") and entry.name.endswith(".jar"): + logging.info(f"Found AP randomizer mod: {entry.name}") + return entry.name return None else: os.mkdir(mods_dir) @@ -77,37 +67,57 @@ def replace_apmc_files(forge_dir, apmc_file): logging.info(f"Copied {os.path.basename(apmc_file)} to {apdata_dir}") +def read_apmc_file(apmc_file): + from base64 import b64decode + import json + + with open(apmc_file, 'r') as f: + data = json.loads(b64decode(f.read())) + return data + + # Check mod version, download new mod from GitHub releases page if needed. -def update_mod(forge_dir): +def update_mod(forge_dir, apmc_file, get_prereleases=False): ap_randomizer = find_ap_randomizer_jar(forge_dir) + if apmc_file is not None: + data = read_apmc_file(apmc_file) + minecraft_version = data.get('minecraft_version', '') + client_releases_endpoint = "https://api.github.com/repos/KonoTyran/Minecraft_AP_Randomizer/releases" resp = requests.get(client_releases_endpoint) if resp.status_code == 200: # OK - latest_release = resp.json()[0] - if ap_randomizer != latest_release['assets'][0]['name']: - logging.info(f"A new release of the Minecraft AP randomizer mod was found: " - f"{latest_release['assets'][0]['name']}") - if ap_randomizer is not None: - logging.info(f"Your current mod is {ap_randomizer}.") - else: - logging.info(f"You do not have the AP randomizer mod installed.") - if prompt_yes_no("Would you like to update?"): - old_ap_mod = os.path.join(forge_dir, 'mods', ap_randomizer) if ap_randomizer is not None else None - new_ap_mod = os.path.join(forge_dir, 'mods', latest_release['assets'][0]['name']) - logging.info("Downloading AP randomizer mod. This may take a moment...") - apmod_resp = requests.get(latest_release['assets'][0]['browser_download_url']) - if apmod_resp.status_code == 200: - with open(new_ap_mod, 'wb') as f: - f.write(apmod_resp.content) - logging.info(f"Wrote new mod file to {new_ap_mod}") - if old_ap_mod is not None: - os.remove(old_ap_mod) - logging.info(f"Removed old mod file from {old_ap_mod}") + try: + latest_release = next(filter(lambda release: (not release['prerelease'] or get_prereleases) and + (apmc_file is None or minecraft_version in release['assets'][0]['name']), + resp.json())) + if ap_randomizer != latest_release['assets'][0]['name']: + logging.info(f"A new release of the Minecraft AP randomizer mod was found: " + f"{latest_release['assets'][0]['name']}") + if ap_randomizer is not None: + logging.info(f"Your current mod is {ap_randomizer}.") else: - logging.error(f"Error retrieving the randomizer mod (status code {apmod_resp.status_code}).") - logging.error(f"Please report this issue on the Archipelago Discord server.") - sys.exit(1) + logging.info(f"You do not have the AP randomizer mod installed.") + if prompt_yes_no("Would you like to update?"): + old_ap_mod = os.path.join(forge_dir, 'mods', ap_randomizer) if ap_randomizer is not None else None + new_ap_mod = os.path.join(forge_dir, 'mods', latest_release['assets'][0]['name']) + logging.info("Downloading AP randomizer mod. This may take a moment...") + apmod_resp = requests.get(latest_release['assets'][0]['browser_download_url']) + if apmod_resp.status_code == 200: + with open(new_ap_mod, 'wb') as f: + f.write(apmod_resp.content) + logging.info(f"Wrote new mod file to {new_ap_mod}") + if old_ap_mod is not None: + os.remove(old_ap_mod) + logging.info(f"Removed old mod file from {old_ap_mod}") + else: + logging.error(f"Error retrieving the randomizer mod (status code {apmod_resp.status_code}).") + logging.error(f"Please report this issue on the Archipelago Discord server.") + sys.exit(1) + except StopIteration: + logging.warning(f"No compatible mod version found for {minecraft_version}.") + if not prompt_yes_no("Run server anyway?"): + sys.exit(0) else: logging.error(f"Error checking for randomizer mod updates (status code {resp.status_code}).") logging.error(f"If this was not expected, please report this issue on the Archipelago Discord server.") @@ -139,11 +149,69 @@ def check_eula(forge_dir): sys.exit(0) -# Run the Forge server. Return process object -def run_forge_server(forge_dir, heap_arg): - forge_server = find_forge_jar(forge_dir) +# get the current JDK16 +def find_jdk_dir() -> str: + for entry in os.listdir(): + if os.path.isdir(entry) and entry.startswith("jdk16"): + return os.path.abspath(entry) - java_exe = os.path.abspath(os.path.join('jre8', 'bin', 'java.exe')) + +# get the java exe location +def find_jdk() -> str: + jdk = find_jdk_dir() + jdk_exe = os.path.join(jdk, "bin", "java.exe") + if os.path.isfile(jdk_exe): + return jdk_exe + + +# Download Corretto 16 (Amazon JDK) +def download_java(): + jdk = find_jdk_dir() + if jdk is not None: + print(f"Removing old JDK...") + from shutil import rmtree + rmtree(jdk) + + print(f"Downloading Java...") + jdk_url = "https://corretto.aws/downloads/latest/amazon-corretto-16-x64-windows-jdk.zip" + resp = requests.get(jdk_url) + if resp.status_code == 200: # OK + print(f"Extracting...") + import zipfile + from io import BytesIO + with zipfile.ZipFile(BytesIO(resp.content)) as zf: + zf.extractall() + else: + print(f"Error downloading Java (status code {resp.status_code}).") + print(f"If this was not expected, please report this issue on the Archipelago Discord server.") + if not prompt_yes_no("Continue anyways?"): + sys.exit(0) + + +# download and install forge +def install_forge(directory: str): + jdk = find_jdk() + if jdk is not None: + print(f"Downloading Forge {forge_version}...") + forge_url = f"https://maven.minecraftforge.net/net/minecraftforge/forge/{forge_version}/forge-{forge_version}-installer.jar" + resp = requests.get(forge_url) + if resp.status_code == 200: # OK + forge_install_jar = os.path.join(directory, "forge_install.jar") + if not os.path.exists(directory): + os.mkdir(directory) + with open(forge_install_jar, 'wb') as f: + f.write(resp.content) + print(f"Installing Forge...") + argstring = ' '.join([jdk, "-jar", "\"" + forge_install_jar+ "\"", "--installServer", "\"" + directory + "\""]) + install_process = Popen(argstring) + install_process.wait() + os.remove(forge_install_jar) + + +# Run the Forge server. Return process object +def run_forge_server(forge_dir: str, heap_arg): + + java_exe = find_jdk() if not os.path.isfile(java_exe): java_exe = "java" # try to fall back on java in the PATH @@ -152,7 +220,13 @@ def run_forge_server(forge_dir, heap_arg): heap_arg = heap_arg[:-1] heap_arg = "-Xmx" + heap_arg - argstring = ' '.join([java_exe, heap_arg, "-jar", forge_server, "-nogui"]) + args_file = os.path.join(forge_dir, "libraries", "net", "minecraftforge", "forge", forge_version, "win_args.txt") + win_args = [] + with open(args_file) as argfile: + for line in argfile: + win_args.append(line.strip()) + + argstring = ' '.join([java_exe, heap_arg] + win_args + ["-nogui"]) logging.info(f"Running Forge server: {argstring}") os.chdir(forge_dir) return Popen(argstring) @@ -162,6 +236,10 @@ if __name__ == '__main__': Utils.init_logging("MinecraftClient") parser = argparse.ArgumentParser() parser.add_argument("apmc_file", default=None, nargs='?', help="Path to an Archipelago Minecraft data file (.apmc)") + parser.add_argument('--install', '-i', dest='install', default=False, action='store_true', + help="Download and install Java and the Forge server. Does not launch the client afterwards.") + parser.add_argument('--prerelease', default=False, action='store_true', + help="Auto-update prerelease versions.") args = parser.parse_args() apmc_file = os.path.abspath(args.apmc_file) if args.apmc_file else None @@ -173,6 +251,12 @@ if __name__ == '__main__': forge_dir = options["minecraft_options"]["forge_directory"] max_heap = options["minecraft_options"]["max_heap_size"] + if args.install: + print("Installing Java and Minecraft Forge") + download_java() + install_forge(forge_dir) + sys.exit(0) + if apmc_file is not None and not os.path.isfile(apmc_file): raise FileNotFoundError(f"Path {apmc_file} does not exist or could not be accessed.") if not os.path.isdir(forge_dir): @@ -180,7 +264,7 @@ if __name__ == '__main__': if not max_heap_re.match(max_heap): raise Exception(f"Max heap size {max_heap} in incorrect format. Use a number followed by M or G, e.g. 512M or 2G.") - update_mod(forge_dir) + update_mod(forge_dir, apmc_file, args.prerelease) replace_apmc_files(forge_dir, apmc_file) check_eula(forge_dir) server_process = run_forge_server(forge_dir, max_heap) diff --git a/MultiServer.py b/MultiServer.py index 5e953e6a..f1fbce85 100644 --- a/MultiServer.py +++ b/MultiServer.py @@ -22,8 +22,7 @@ ModuleUpdate.update() import websockets import colorama -import prompt_toolkit -from prompt_toolkit.patch_stdout import patch_stdout + from fuzzywuzzy import process as fuzzy_process import NetUtils diff --git a/NetUtils.py b/NetUtils.py index d4f90b2a..ff49b8b8 100644 --- a/NetUtils.py +++ b/NetUtils.py @@ -1,4 +1,6 @@ from __future__ import annotations + +import logging import typing import enum from json import JSONEncoder, JSONDecoder @@ -138,11 +140,11 @@ class HandlerMeta(type): break def __init__(self, *args, **kwargs): + if orig_init: + orig_init(self, *args, **kwargs) # turn functions into bound methods self.handlers = {name: method.__get__(self, type(self)) for name, method in handlers.items()} - if orig_init: - orig_init(self, *args, **kwargs) attrs['__init__'] = __init__ return super(HandlerMeta, mcs).__new__(mcs, name, bases, attrs) @@ -192,11 +194,7 @@ class JSONtoTextParser(metaclass=HandlerMeta): return self._handle_color(node) def _handle_item_name(self, node: JSONMessagePart): - # todo: use a better info source - from worlds.alttp.Items import progression_items - node["color"] = 'green' if node.get("found", False) else 'cyan' - if node["text"] in progression_items: - node["color"] += ";white_bg" + node["color"] = 'cyan' return self._handle_color(node) def _handle_item_id(self, node: JSONMessagePart): @@ -205,13 +203,13 @@ class JSONtoTextParser(metaclass=HandlerMeta): return self._handle_item_name(node) def _handle_location_name(self, node: JSONMessagePart): - node["color"] = 'blue_bg;white' + node["color"] = 'green' return self._handle_color(node) def _handle_location_id(self, node: JSONMessagePart): item_id = int(node["text"]) node["text"] = self.ctx.location_name_getter(item_id) - return self._handle_item_name(node) + return self._handle_location_name(node) def _handle_entrance_name(self, node: JSONMessagePart): node["color"] = 'blue' @@ -282,10 +280,11 @@ class Hint(typing.NamedTuple): add_json_text(parts, self.entrance, type="entrance_name") else: add_json_text(parts, "'s World") + add_json_text(parts, ". ") if self.found: - add_json_text(parts, ". (found)") + add_json_text(parts, "(found)", type="color", color="green") else: - add_json_text(parts, ".") + add_json_text(parts, "(not found)", type="color", color="red") return {"cmd": "PrintJSON", "data": parts, "type": "Hint", "receiving": self.receiving_player, diff --git a/Options.py b/Options.py index 9fc2da5a..cd4d9148 100644 --- a/Options.py +++ b/Options.py @@ -277,8 +277,8 @@ class OptionList(Option): supports_weighting = False value: list - def __init__(self, value: typing.List[str, typing.Any]): - self.value = value + def __init__(self, value: typing.List[typing.Any]): + self.value = value or [] super(OptionList, self).__init__() @classmethod @@ -292,7 +292,7 @@ class OptionList(Option): return cls.from_text(str(data)) def get_option_name(self, value): - return ", ".join(value) + return ", ".join(map(str, value)) def __contains__(self, item): return item in self.value diff --git a/Patch.py b/Patch.py index 09f41277..e9b3c800 100644 --- a/Patch.py +++ b/Patch.py @@ -87,7 +87,7 @@ def get_base_rom_data(game: str): elif game == GAME_SM: from worlds.sm.Rom import get_base_rom_bytes elif game == GAME_SOE: - file_name = Utils.get_options()["soe_options"]["rom"] + file_name = Utils.get_options()["soe_options"]["rom_file"] get_base_rom_bytes = lambda: bytes(read_rom(open(file_name, "rb"))) else: raise RuntimeError("Selected game for base rom not found.") diff --git a/README.md b/README.md index 8135798d..739a8caf 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,7 @@ Currently, the following games are supported: * Timespinner * Super Metroid * Secret of Evermore +* Final Fantasy For setup and instructions check out our [tutorials page](https://archipelago.gg/tutorial/). Downloads can be found at [Releases](https://github.com/ArchipelagoMW/Archipelago/releases), including compiled diff --git a/SNIClient.py b/SNIClient.py index 61262afe..6d7540ea 100644 --- a/SNIClient.py +++ b/SNIClient.py @@ -175,15 +175,22 @@ async def deathlink_kill_player(ctx: Context): snes_buffered_write(ctx, WRAM_START + 0x0373, bytes([8])) # deal 1 full heart of damage at next opportunity elif ctx.game == GAME_SM: snes_buffered_write(ctx, WRAM_START + 0x09C2, bytes([0, 0])) # set current health to 0 + if not ctx.death_link_allow_survive: + snes_buffered_write(ctx, WRAM_START + 0x09D6, bytes([0, 0])) # set current reserve to 0 await snes_flush_writes(ctx) await asyncio.sleep(1) gamemode = None if ctx.game == GAME_ALTTP: gamemode = await snes_read(ctx, WRAM_START + 0x10, 1) + if not gamemode or gamemode[0] in DEATH_MODES: + ctx.death_state = DeathState.dead elif ctx.game == GAME_SM: gamemode = await snes_read(ctx, WRAM_START + 0x0998, 1) - if not gamemode or gamemode[0] in (DEATH_MODES if ctx.game == GAME_ALTTP else SM_DEATH_MODES): - ctx.death_state = DeathState.dead + health = await snes_read(ctx, WRAM_START + 0x09C2, 2) + if health is not None: + health = health[0] | (health[1] << 8) + if not gamemode or gamemode[0] in SM_DEATH_MODES or (ctx.death_link_allow_survive and health is not None and health > 0): + ctx.death_state = DeathState.dead ctx.last_death_link = time.time() @@ -884,6 +891,7 @@ async def game_watcher(ctx: Context): if not ctx.rom: ctx.finished_game = False + ctx.death_link_allow_survive = False game_name = await snes_read(ctx, SM_ROMNAME_START, 2) if game_name is None: continue @@ -900,6 +908,7 @@ async def game_watcher(ctx: Context): death_link = await snes_read(ctx, DEATH_LINK_ACTIVE_ADDR if ctx.game == GAME_ALTTP else SM_DEATH_LINK_ACTIVE_ADDR, 1) if death_link: + ctx.death_link_allow_survive = bool(death_link[0] & 0b10) await ctx.update_death_link(bool(death_link[0] & 0b1)) if not ctx.prev_rom or ctx.prev_rom != ctx.rom: ctx.locations_checked = set() @@ -1039,6 +1048,7 @@ async def game_watcher(ctx: Context): ctx.location_name_getter(item.location), itemOutPtr, len(ctx.items_received))) await snes_flush_writes(ctx) + async def run_game(romfile): auto_start = Utils.get_options()["lttp_options"].get("rom_start", True) if auto_start is True: @@ -1062,11 +1072,13 @@ async def main(): import Patch logging.info("Patch file was supplied. Creating sfc rom..") meta, romfile = Patch.create_rom_file(args.diff_file) - args.connect = meta["server"] + if "server" in meta: + args.connect = meta["server"] logging.info(f"Wrote rom file to {romfile}") if args.diff_file.endswith(".apsoe"): import webbrowser - webbrowser.open("http://www.evermizer.com/apclient/") + webbrowser.open("http://www.evermizer.com/apclient/" + + (f"#server={meta['server']}" if "server" in meta else "")) logging.info("Starting Evermizer Client in your Browser...") import time time.sleep(3) diff --git a/Utils.py b/Utils.py index 29491c62..02a25e70 100644 --- a/Utils.py +++ b/Utils.py @@ -23,7 +23,7 @@ class Version(typing.NamedTuple): build: int -__version__ = "0.2.0" +__version__ = "0.2.1" version_tuple = tuplize_version(__version__) from yaml import load, dump, safe_load diff --git a/WebHost.py b/WebHost.py index 1962a1cd..261f82b5 100644 --- a/WebHost.py +++ b/WebHost.py @@ -14,7 +14,7 @@ from WebHostLib import app as raw_app from waitress import serve from WebHostLib.models import db -from WebHostLib.autolauncher import autohost +from WebHostLib.autolauncher import autohost, autogen from WebHostLib.lttpsprites import update_sprites_lttp from WebHostLib.options import create as create_options_files @@ -45,6 +45,8 @@ if __name__ == "__main__": create_options_files() if app.config["SELFLAUNCH"]: autohost(app.config) + if app.config["SELFGEN"]: + autogen(app.config) if app.config["SELFHOST"]: # using WSGI, you just want to run get_app() if app.config["DEBUG"]: autohost(app.config) diff --git a/WebHostLib/__init__.py b/WebHostLib/__init__.py index d32267d2..235b4f52 100644 --- a/WebHostLib/__init__.py +++ b/WebHostLib/__init__.py @@ -22,9 +22,10 @@ Pony(app) app.jinja_env.filters['any'] = any app.jinja_env.filters['all'] = all -app.config["SELFHOST"] = True +app.config["SELFHOST"] = True # application process is in charge of running the websites app.config["GENERATORS"] = 8 # maximum concurrent world gens -app.config["SELFLAUNCH"] = True +app.config["SELFLAUNCH"] = True # application process is in charge of launching Rooms. +app.config["SELFGEN"] = True # application process is in charge of scheduling Generations. app.config["DEBUG"] = False app.config["PORT"] = 80 app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER @@ -166,8 +167,9 @@ def host_room(room: UUID): if request.method == "POST": if room.owner == session["_id"]: cmd = request.form["cmd"] - Command(room=room, commandtext=cmd) - commit() + if cmd: + Command(room=room, commandtext=cmd) + commit() with db_session: room.last_activity = datetime.utcnow() # will trigger a spinup, if it's not already running diff --git a/WebHostLib/autolauncher.py b/WebHostLib/autolauncher.py index 0c1c2b6d..97db4ca6 100644 --- a/WebHostLib/autolauncher.py +++ b/WebHostLib/autolauncher.py @@ -110,6 +110,26 @@ def autohost(config: dict): def keep_running(): try: with Locker("autohost"): + while 1: + time.sleep(0.1) + with db_session: + rooms = select( + room for room in Room if + room.last_activity >= datetime.utcnow() - timedelta(days=3)) + for room in rooms: + launch_room(room, config) + + except AlreadyRunningException: + logging.info("Autohost reports as already running, not starting another.") + + import threading + threading.Thread(target=keep_running, name="AP_Autohost").start() + + +def autogen(config: dict): + def keep_running(): + try: + with Locker("autogen"): with multiprocessing.Pool(config["GENERATORS"], initializer=init_db, initargs=(config["PONY"],)) as generator_pool: @@ -129,22 +149,17 @@ def autohost(config: dict): select(generation for generation in Generation if generation.state == STATE_ERROR).delete() while 1: - time.sleep(0.50) + time.sleep(0.1) with db_session: - rooms = select( - room for room in Room if - room.last_activity >= datetime.utcnow() - timedelta(days=3)) - for room in rooms: - launch_room(room, config) to_start = select( generation for generation in Generation if generation.state == STATE_QUEUED) for generation in to_start: launch_generator(generator_pool, generation) except AlreadyRunningException: - pass + logging.info("Autogen reports as already running, not starting another.") import threading - threading.Thread(target=keep_running).start() + threading.Thread(target=keep_running, name="AP_Autogen").start() multiworlds = {} diff --git a/WebHostLib/customserver.py b/WebHostLib/customserver.py index 4ddf01ed..cfc5de81 100644 --- a/WebHostLib/customserver.py +++ b/WebHostLib/customserver.py @@ -2,7 +2,6 @@ from __future__ import annotations import functools import logging -import os import websockets import asyncio import socket @@ -20,6 +19,7 @@ from Utils import get_public_ipv4, get_public_ipv6, restricted_loads class CustomClientMessageProcessor(ClientMessageProcessor): ctx: WebHostContext + def _cmd_video(self, platform, user): """Set a link for your name in the WebHostLib tracker pointing to a video stream""" if platform.lower().startswith("t"): # twitch @@ -37,6 +37,7 @@ class CustomClientMessageProcessor(ClientMessageProcessor): # inject import MultiServer + MultiServer.client_message_processor = CustomClientMessageProcessor del (MultiServer) @@ -88,11 +89,11 @@ class WebHostContext(Context): threading.Thread(target=self.listen_to_db_commands, daemon=True).start() @db_session - def _save(self, exit_save:bool = False) -> bool: + def _save(self, exit_save: bool = False) -> bool: room = Room.get(id=self.room_id) room.multisave = pickle.dumps(self.get_save()) # saving only occurs on activity, so we can "abuse" this information to mark this as last_activity - if not exit_save: # we don't want to count a shutdown as activity, which would restart the server again + if not exit_save: # we don't want to count a shutdown as activity, which would restart the server again room.last_activity = datetime.utcnow() return True @@ -101,6 +102,7 @@ class WebHostContext(Context): d["video"] = [(tuple(playerslot), videodata) for playerslot, videodata in self.video.items()] return d + def get_random_port(): return random.randint(49152, 65535) diff --git a/WebHostLib/lttpsprites.py b/WebHostLib/lttpsprites.py index 81eb9c2d..ead68f86 100644 --- a/WebHostLib/lttpsprites.py +++ b/WebHostLib/lttpsprites.py @@ -10,6 +10,7 @@ def update_sprites_lttp(): from tkinter import Tk from LttPAdjuster import get_image_for_sprite from LttPAdjuster import BackgroundTaskProgress + from LttPAdjuster import BackgroundTaskProgressNullWindow from LttPAdjuster import update_sprites # Target directories @@ -19,11 +20,15 @@ def update_sprites_lttp(): os.makedirs(os.path.join(output_dir, "sprites"), exist_ok=True) # update sprites through gui.py's functions done = threading.Event() - top = Tk() - top.withdraw() - BackgroundTaskProgress(top, update_sprites, "Updating Sprites", lambda succesful, resultmessage: done.set()) + try: + top = Tk() + except: + task = BackgroundTaskProgressNullWindow(update_sprites, lambda successful, resultmessage: done.set()) + else: + top.withdraw() + task = BackgroundTaskProgress(top, update_sprites, "Updating Sprites", lambda succesful, resultmessage: done.set()) while not done.isSet(): - top.update() + task.do_events() spriteData = [] diff --git a/WebHostLib/static/assets/gameInfo/en_Final Fantasy.md b/WebHostLib/static/assets/gameInfo/en_Final Fantasy.md index 087d37a4..e25981a1 100644 --- a/WebHostLib/static/assets/gameInfo/en_Final Fantasy.md +++ b/WebHostLib/static/assets/gameInfo/en_Final Fantasy.md @@ -6,12 +6,9 @@ Unlike most games on Archipelago.gg, Final Fantasy 1's settings are controlled e ## What does randomization do to this game? A better questions is what isn't randomized at this point. Enemies stats and spell, character spells, shop inventory and boss stats and spells are all commonly randomized. Unlike most other randomizers it is also most standard to shuffle progression items and non-progression items into separate pools and then redistribute them to their respective locations. So ,for example, Princess Sarah may have the CANOE instead of the LUTE; however, she will never have a Heal Pot or some armor. There are plenty of other things that can be randomized on the main randomizer site: [FF1R Website](https://finalfantasyrandomizer.com/) -Some features are not currently supported by AP. A non-exhaustive list includes: -- Shard Hunt -- Deep Dungeon - ## What Final Fantasy items can appear in other players' worlds? -Currently, only progression items can appear in other players' worlds. Armor, Weapons and Consumable Items can not. +All items can appear in other players worlds. This includes consumables, shards, weapons, armor and, of course, +key items. ## What does another world's item look like in Final Fantasy All local and remote items appear the same. It will say that you received an item and then BOTH the client log and the emulator will display what was found external to the in-game text box. diff --git a/WebHostLib/static/assets/gameInfo/en_Secret of Evermore.md b/WebHostLib/static/assets/gameInfo/en_Secret of Evermore.md index 7c5c0335..c1f91f85 100644 --- a/WebHostLib/static/assets/gameInfo/en_Secret of Evermore.md +++ b/WebHostLib/static/assets/gameInfo/en_Secret of Evermore.md @@ -6,7 +6,7 @@ The player settings page for this game contains all the options you need to conf ## What does randomization do to this game? Items which would normally be acquired throughout the game have been moved around! Progression logic remains, so the game is always able to be completed. However, because of the item shuffle, the player may need to access certain areas before they would in the vanilla game. For example, the Windwalker (flying machine) is accessible as soon as any weapon is obtained. -Additional help can be found in the [Evermizer guide](https://github.com/black-sliver/evermizer/blob/feat-mw/guide.md). +Additional help can be found in the [Evermizer guide](https://github.com/black-sliver/evermizer/blob/master/guide.md). ## What items and locations get shuffled? All gourds/chests/pots, boss drops and alchemists are shuffled. Alchemy ingredients, sniff spot items, call bead spells and the dog can be randomized using yaml options. diff --git a/WebHostLib/static/assets/supermetroidTracker.js b/WebHostLib/static/assets/supermetroidTracker.js new file mode 100644 index 00000000..a698214b --- /dev/null +++ b/WebHostLib/static/assets/supermetroidTracker.js @@ -0,0 +1,49 @@ +window.addEventListener('load', () => { + // Reload tracker every 15 seconds + const url = window.location; + setInterval(() => { + const ajax = new XMLHttpRequest(); + ajax.onreadystatechange = () => { + if (ajax.readyState !== 4) { return; } + + // Create a fake DOM using the returned HTML + const domParser = new DOMParser(); + const fakeDOM = domParser.parseFromString(ajax.responseText, 'text/html'); + + // Update item tracker + document.getElementById('inventory-table').innerHTML = fakeDOM.getElementById('inventory-table').innerHTML; + // Update only counters in the location-table + let counters = document.getElementsByClassName('counter'); + const fakeCounters = fakeDOM.getElementsByClassName('counter'); + for (let i = 0; i < counters.length; i++) { + counters[i].innerHTML = fakeCounters[i].innerHTML; + } + }; + ajax.open('GET', url); + ajax.send(); + }, 15000) + + // Collapsible advancement sections + const categories = document.getElementsByClassName("location-category"); + for (let i = 0; i < categories.length; i++) { + let hide_id = categories[i].id.split('-')[0]; + if (hide_id == 'Total') { + continue; + } + categories[i].addEventListener('click', function() { + // Toggle the advancement list + document.getElementById(hide_id).classList.toggle("hide"); + // Change text of the header + const tab_header = document.getElementById(hide_id+'-header').children[0]; + const orig_text = tab_header.innerHTML; + let new_text; + if (orig_text.includes("▼")) { + new_text = orig_text.replace("▼", "▲"); + } + else { + new_text = orig_text.replace("▲", "▼"); + } + tab_header.innerHTML = new_text; + }); + } +}); diff --git a/WebHostLib/static/assets/tutorial/archipelago/advanced_settings_en.md b/WebHostLib/static/assets/tutorial/archipelago/advanced_settings_en.md index f1ad9b8a..3763d43c 100644 --- a/WebHostLib/static/assets/tutorial/archipelago/advanced_settings_en.md +++ b/WebHostLib/static/assets/tutorial/archipelago/advanced_settings_en.md @@ -1,23 +1,23 @@ -# Advanced YAML Guide -This guide covers more the more advanced options available in YAML files. This guide is intended for the user who is intent on editing their YAML file manually. This guide should take about 10 minutes to read. +# Advanced Game Options Guide -If you would like to generate a basic, fully playable, YAML without editing a file then visit the settings page for the game you intend to play. -The settings page can be found on the supported games page, just click the "Settings Page" link under the name of the game you would like. Supported games page: [Archipelago Games List](https://archipelago.gg/games) - -Clicking on the "Export Settings" button at the bottom-left will provide you with a pre-filled YAML with your options. The player settings page also has an option to download a fully filled out yaml containing every option with every available setting for the available options. - -## YAML Overview -The Archipelago system generates games using player configuration files as input. These are going to be YAML files and each world will have one of these containing their custom settings for the game that world will play. +The Archipelago system generates games using player configuration files as input. Generally these are going to be +YAML files and each player will have one of these containing their custom settings for the randomized game they want to play. +On the website when you customize your settings from one of the game player settings pages which you can reach from the +[supported games page](/games). Clicking on the export settings button at the bottom will provide you with a pre-filled out +YAML with your options. The player settings page also has an option to download a fully filled out yaml containing every +option with every available setting for the available options. ## YAML Formatting -YAML files are a format of human-readable config files. The basic syntax of a yaml file will have a `root` node and then different levels of `nested` nodes that the generator reads in order to determine your settings. +YAML files are a format of human-readable markup config files. The basic syntax +of a yaml file will have `root` and then different levels of `nested` text that the generator "reads" in order to determine +your settings. To nest text, the correct syntax is **two spaces over** from its root option. A YAML file can be edited +with whatever text editor you choose to use though I personally recommend that you use [Sublime Text](https://www.sublimetext.com/). +This program out of the box supports the correct formatting for the YAML file, so you will be able to tab and get proper +highlighting for any potential errors made while editing the file. If using any other text editor such as Notepad or +Notepad++ whenever you move to nest an option that it is done with two spaces and not tabs. -To nest text, the correct syntax is to indent **two spaces over** from its root option. A YAML file can be edited with whatever text editor you choose to use though I personally recommend that you use Sublime Text. Sublime text website: [SublimeText Website](https://www.sublimetext.com) - -This program out of the box supports the correct formatting for the YAML file, so you will be able to use the tab key and get proper highlighting for any potential errors made while editing the file. If using any other text editor you should ensure your indentation is done correctly with two spaces. - -A typical YAML file will look like: +Typical YAML format will look as follows: ```yaml root_option: nested_option_one: @@ -28,38 +28,49 @@ root_option: option_two_setting_two: 43 ``` -In Archipelago, YAML options are always written out in full lowercase with underscores separating any words. The numbers following the colons here are weights. The generator will read the weight of every option the roll that option that many times, the next option as many times as its numbered and so forth. - -For the above example `nested_option_one` will have `option_one_setting_one` 1 time and `option_one_setting_two` 0 times so `option_one_setting_one` is guaranteed to occur. - -For `nested_option_two`, `option_two_setting_one` will be rolled 14 times and `option_two_setting_two` will be rolled 43 times against each other. This means `option_two_setting_two` will be more likely to occur, but it isn't guaranteed adding more randomness and "mystery" to your settings. - -Every configurable setting supports weights. +In Archipelago YAML options are always written out in full lowercase with underscores separating any words. The numbers +following the colons here are weights. The generator will read the weight of every option the roll that option that many +times, the next option as many times as its numbered and so forth. For the above example `nested_option_one` will have +`option_one_setting_one` 1 time and `option_one_setting_two` 0 times so `option_one_setting_one` is guaranteed to occur. +For `nested_option_two`, `option_two_setting_one` will be rolled 14 times and `option_two_setting_two` will be rolled 43 +times against each other. This means `option_two_setting_two` will be more likely to occur but it isn't guaranteed adding +more randomness and "mystery" to your settings. Every configurable setting supports weights. ### Root Options -Currently, there are only a few options that are root options. Everything else should be nested within one of these root options or in some cases nested within other nested options. The only options that should exist in root are `description`, `name`, `game`, `requires`, `accessibility`, `progression_balancing`, `triggers`, and the name of the games you want settings for. - -* `description` is ignored by the generator and is simply a good way for you to organize if you have multiple files using this to detail the intention of the file. - -* `name` is the player name you would like to use and is used for your slot data to connect with most games. This can also be filled with multiple names each having a weight to it. - -* `game` is where either your chosen game goes or if you would like can be filled with multiple games each with different weights. - -* `requires` details different requirements from the generator for the YAML to work as you expect it to. Generally this is good for detailing the version of Archipelago this YAML was prepared for as if it is rolled on an older version may be missing settings and as such will not work as expected. If any plando is used in the file then requiring it here to ensure it will be used is good practice. - -* `accessibility` determines the level of access to the game the generation will expect you to have in order to reach your completion goal. This supports `items`, `locations`, and `none` and is set to `locations` by default. - * `locations` will guarantee all locations are accessible in your world. - * `items` will guarantee you can acquire all items in your world but may not be able to access all locations. This mostly comes into play if there is any entrance shuffle in the seed as locations without items in them can be placed in areas that make them unreachable. - * `none` will only guarantee that the seed is beatable. You will be guaranteed able to finish the seed logically but may not be able to access all locations or acquire all items. A good example of this is having a big key in the big chest +Currently there are only a few options that are root options. Everything else should be nested within one of these root +options or in some cases nested within other nested options. The only options that should exist in root are `description`, +`name`, `game`, `requires`, `accessibility`, `progression_balancing`, `triggers`, and the name of the games you want +settings for. +* `description` is ignored by the generator and is simply a good way for you to organize if you have multiple files using +this to detail the intention of the file. +* `name` is the player name you would like to use and is used for your slot data to connect with most games. This can also +be filled with multiple names each having a weight to it. +* `game` is where either your chosen game goes or if you would like can be filled with multiple games each with different +weights. +* `requires` details different requirements from the generator for the YAML to work as you expect it to. Generally this +is good for detailing the version of Archipelago this YAML was prepared for as if it is rolled on an older version may be +missing settings and as such will not work as expected. If any plando is used in the file then requiring it here to ensure +it will be used is good practice. +* `accessibility` determines the level of access to the game the generation will expect you to have in order to reach your +completion goal. This supports `items`, `locations`, and `none` and is set to `locations` by default. + * `items` will guarantee you can acquire all items in your world but may not be able to access all locations. This mostly +comes into play if there is any entrance shuffle in the seed as locations without items in them can be placed in areas +that make them unreachable. + * `none` will only guarantee that the seed is beatable. You will be guaranteed able to finish the seed logically but +may not be able to access all locations or acquire all items. A good example of this is having a big key in the big chest in a dungeon in ALTTP making it impossible to get and finish the dungeon. - -* `progression_balancing` is a system the Archipelago generator uses to try and reduce "BK mode" as much as possible. This primarily involves moving necessary progression items into earlier logic spheres to make the games more accessible so that players almost always have something to do. This can be turned `on` or `off` and is `on` by default. - - * `triggers` is one of the more advanced options that allows you to create conditional adjustments. You can read more triggers in the triggers guide. Triggers guide: [Archipelago Triggers Guide](https://archipelago.gg/tutorial/archipelago/triggers/en) +* `progression_balancing` is a system the Archipelago generator uses to try and reduce "BK mode" as much as possible. This +primarily involves moving necessary progression items into earlier logic spheres to make the games more accessible so that +players almost always have something to do. This can be turned `on` or `off` and is `on` by default. +* `triggers` is one of the more advanced options that allows you to create conditional adjustments. You can read more +about this [here](/tutorial/archipelago/triggers/en). ### Game Options -One of your root settings will be the name of the game you would like to populate with settings. Since it is possible to give a weight to any option it is possible to have one file that can generate a seed for you where you don't know which game you'll play. For these cases you'll want to fill the game options for every game that can be rolled by these settings. If a game can be rolled it **must** have a settings section even if it is empty. +One of your root settings will be the name of the game you would like to populate with settings in the format +`GameName`. since it is possible to give a weight to any option it is possible to have one file that can generate a seed +for you where you don't know which game you'll play. For these cases you'll want to fill the game options for every game +that can be rolled by these settings. If a game can be rolled it **must** have a settings section even if it is empty. #### Universal Game Options @@ -67,21 +78,21 @@ Some options in Archipelago can be used by every game but must still be placed w Currently, these options are `start_inventory`, `start_hints`, `local_items`, `non_local_items`, `start_location_hints`, `exclude_locations`, and various plando options. -See the plando guide for more info on plando options. Plando guide: [Archipelago Plando Guide](https://archipelago.gg/tutorial/archipelago/plando/en) +See the plando guide for more info on plando options. Plando guide: [Archipelago Plando Guide](/tutorial/archipelago/plando/en) * `start_inventory` will give any items defined here to you at the beginning of your game. The format for this must be the name as it appears in the game files and the amount you would like to start with. For example `Rupees(5): 6` which will give you the item `Rupees(5)` six times, totalling 30 rupees. * `start_hints` gives you free server hints for the defined item/s at the beginning of the game allowing you to hint for the location without using any hint points. * `local_items` will force any items you want to be in your world instead of being in another world. +* `non_local_items` is the inverse of `local_items` forcing any items you want to be in another world and won't be located +in your own. +* `start_location_hints` allows you to define a location which you can then hint for to find out what item is located in +it to see how important the location is. +* `exclude_locations` lets you define any locations that you don't want to do and during generation will force a "junk" +item which isn't necessary for progression to go in these locations. -* `non_local_items` is the inverse of `local_items` forcing any items you want to be in another world and won't be located in your own. - -* `start_location_hints` allows you to define a location which you can then hint for to find out what item is located in it to see how important the location is. - -* `exclude_locations` lets you define any locations that you don't want to do and during generation will force a "junk" item which isn't necessary for progression to go in these locations. - -### Example YAML +### Example ```yaml @@ -124,33 +135,28 @@ triggers: #### This is a fully functional yaml file that will do all the following things: * `description` gives us a general overview so if we pull up this file later we can understand the intent. - * `name` is `Example Player` and this will be used in the server console when sending and receiving items. - * `game` is set to `A Link to the Past` meaning that is what game we will play with this file. - * `requires` is set to require release version 0.2.0 or higher. - -* `accesibility` is set to `none` which will set this seed to beatable only meaning some locations and items may be completely inaccessible but the seed will still be completable. - -* `progression_balancing` is set on meaning we will likely receive important items earlier increasing the chance of having things to do. - +* `accesibility` is set to `none` which will set this seed to beatable only meaning some locations and items may be +completely inaccessible but the seed will still be completable. +* `progression_balancing` is set on meaning we will likely receive important items earlier increasing the chance of having +things to do. * `A Link to the Past` defines a location for us to nest all the game options we would like to use for our game `A Link to the Past`. - -* `smallkey_shuffle` is an option for A Link to the Past which determines how dungeon small keys are shuffled. In this example we have a 1/2 chance for them to either be placed in their original dungeon and a 1/2 chance for them to be placed anywhere amongst the multiworld. - -* `start_inventory` defines an area for us to determine what items we would like to start the seed with. For this example we have: +* `smallkey_shuffle` is an option for A Link to the Past which determines how dungeon small keys are shuffled. In this example +we have a 1/2 chance for them to either be placed in their original dungeon and a 1/2 chance for them to be placed anywhere +amongst the multiworld. +* `start_inventory` defines an area for us to determine what items we would like to start the seed with. For this example +we have: * `Pegasus Boots: 1` which gives us 1 copy of the Pegasus Boots * `Bombs (3)` gives us 2 packs of 3 bombs or 6 total bombs - * `start_hints` gives us a starting hint for the hammer available at the beginning of the multiworld which we can use with no cost. - -* `local_items` forces the `Bombos`, `Ether`, and `Quake` medallions to all be placed within our own world, meaning we have to find it ourselves. - +* `local_items` forces the `Bombos`, `Ether`, and `Quake` medallions to all be placed within our own world, meaning we +have to find it ourselves. * `non_local_items` forces the `Moon Pearl` to be placed in someone else's world, meaning we won't be able to find it. - -* `start_location_hints` gives us a starting hint for the `Spike Cave` location available at the beginning of the multiworld that can be used for no cost. - +* `start_location_hints` gives us a starting hint for the `Spike Cave` location available at the beginning of the multiworld +that can be used for no cost. * `exclude_locations` forces a not important item to be placed on the `Cave 45` location. - -* `triggers` allows us to define a trigger such that if our `smallkey_shuffle` option happens to roll the `any_world` result it will also ensure that `bigkey_shuffle`, `map_shuffle`, and `compass_shuffle` are also forced to the `any_world` result. \ No newline at end of file +* `triggers` allows us to define a trigger such that if our `smallkey_shuffle` option happens to roll the `any_world` +result it will also ensure that `bigkey_shuffle`, `map_shuffle`, and `compass_shuffle` are also forced to the `any_world` +result. \ No newline at end of file diff --git a/WebHostLib/static/assets/tutorial/minecraft/minecraft_en.md b/WebHostLib/static/assets/tutorial/minecraft/minecraft_en.md index 1a7e2e66..08cd4791 100644 --- a/WebHostLib/static/assets/tutorial/minecraft/minecraft_en.md +++ b/WebHostLib/static/assets/tutorial/minecraft/minecraft_en.md @@ -1,86 +1,18 @@ # Minecraft Randomizer Setup Guide -#Automatic Hosting Install -- Download and install Archipelago at: [Archipelago Releases Page](https://github.com/ArchipelagoMW/Archipelago/releases) - - Choose the `Minecraft Client` module during the installation. - ## Required Software -- Minecraft Java Edition from: [Minecraft Java Edition Store Page](https://www.minecraft.net/en-us/store/minecraft-java-edition) +- Minecraft Java Edition from the [Minecraft Java Edition Store Page](https://www.minecraft.net/en-us/store/minecraft-java-edition) (update 1.17.1) +- Archipelago from the [Archipelago Releases Page](https://github.com/ArchipelagoMW/Archipelago/releases) + - (select `Minecraft Client` during installation.) ## Configuring your YAML file ### What is a YAML file and why do I need one? See the guide on setting up a basic YAML at the Archipelago setup guide: [Basic Multiworld Setup Guide](/tutorial/archipelago/setup/en) -### What Does a YAML Look Like for Minecraft? -A basic minecraft yaml will look like this. -```yaml -description: Basic Minecraft Yaml -# Your name in-game. Spaces will be replaced with underscores and -# there is a 16 character limit -name: YourName -game: Minecraft - -# Shared Options supported by all games: -accessibility: locations -progression_balancing: on -# Minecraft Specific Options - -Minecraft: - # Number of advancements required (87 max) to spawn the Ender Dragon and complete the game. - advancement_goal: 50 - - # Number of dragon egg shards to collect (30 max) before the Ender Dragon will spawn. - egg_shards_required: 10 - - # Number of egg shards available in the pool (30 max). - egg_shards_available: 15 - - # Modifies the level of items logically required for - # exploring dangerous areas and fighting bosses. - combat_difficulty: - easy: 0 - normal: 1 - hard: 0 - - # Junk-fills certain RNG-reliant or tedious advancements. - include_hard_advancements: - on: 0 - off: 1 - - # Junk-fills extremely difficult advancements; - # this is only How Did We Get Here? and Adventuring Time. - include_insane_advancements: - on: 0 - off: 1 - - # Some advancements require defeating the Ender Dragon; - # this will junk-fill them, so you won't have to finish them to send some items. - include_postgame_advancements: - on: 0 - off: 1 - - # Enables shuffling of villages, outposts, fortresses, bastions, and end cities. - shuffle_structures: - on: 0 - off: 1 - - # Adds structure compasses to the item pool, - # which point to the nearest indicated structure. - structure_compasses: - on: 0 - off: 1 - - # Replaces a percentage of junk items with bee traps - # which spawn multiple angered bees around every player when received. - bee_traps: - 0: 1 - 25: 0 - 50: 0 - 75: 0 - 100: 0 -``` +### Where do I get a YAML file? +You can customize your settings by visiting the [Minecraft Player Settings Page](/games/Minecraft/player-settings) ## Joining a MultiWorld Game @@ -89,35 +21,23 @@ Minecraft: When you join a multiworld game, you will be asked to provide your YAML file to whoever is hosting. Once that is done, the host will provide you with either a link to download your data file, or with a zip file containing everyone's data files. Your data file should have a `.apmc` extension. -Double-click on your `.apmc` file to have the minecraft client auto-launch the installed forge server. +Double-click on your `.apmc` file to have the Minecraft client auto-launch the installed forge server. Make sure to leave this window open as this is your server console. ### Connect to the MultiServer -If you are running Forge manually, you must place the `.apmc` file in your `APData` folder in the Forge installation directory. If the `APData` folder does not exist then you may create it. After having placed your data file in the `APData` folder, start the Forge server and make sure you have OP status by typing `/op YourMinecraftUsername` in the forge server console then connecting in your Minecraft client. +Using minecraft 1.17.1 connect to the server `localhost`. -In all cases, no matter how the Forge server is hosted: once you are in game type `/connect (Port) (Password)` where `` is the address of the Archipelago server. `(Port)` is only required if the Archipelago server is not using the default port of 38281. `(Password)` is only required if the Archipelago server you are using has a password set. +Once you are in game type `/connect (Port) (Password)` where `` is the address of the Archipelago server. `(Port)` is only required if the Archipelago server is not using the default port of 38281. `(Password)` is only required if the Archipelago server you are using has a password set. ### Play the game -When the console tells you that you have joined the room, you're ready to begin playing. Congratulations on successfully joining a multiworld game! At this point any additional minecraft players may connect to your forge server. When you are ready to start the game use the `/start` command within the Minecraft game. +When the console tells you that you have joined the room, you're all set. Congratulations on successfully joining a multiworld game! At this point any additional minecraft players may connect to your forge server. To start the game once everyone is ready use the command `/start`. -## Manual Installation Procedures -This is only required if you wish to set up a forge install yourself, it's recommended to just use the Archipelago Installer. +## Manual Installation +It is highly recommended to ues the Archipelago installer to handle the installation of the forge server for you. support will not be given for those wishing to manually install forge. For those of you who know how, and wish to do so, the following links are the versions of the software we use. -###Required Software -- Minecraft Forge from: [Minecraft Forge Download Page](https://files.minecraftforge.net/net/minecraftforge/forge/index_1.16.5.html) -- Minecraft Archipelago Randomizer Mod from: [AP Randomizer Forge Server Mod Releases Page](https://github.com/KonoTyran/Minecraft_AP_Randomizer/releases) - - **DO NOT INSTALL THIS ON YOUR CLIENT** +### Manual install Software links +- [Minecraft Forge Download Page](https://files.minecraftforge.net/net/minecraftforge/forge/index_1.17.1.html) +- [Minecraft Archipelago Randomizer Mod Releases Page](https://github.com/KonoTyran/Minecraft_AP_Randomizer/releases) +**DO NOT INSTALL THIS ON YOUR CLIENT** +- [Java 16 Download Page](https://docs.aws.amazon.com/corretto/latest/corretto-16-ug/downloads-list.html) -### Dedicated Server Setup -Only one person has to do this setup and host a dedicated server for everyone else playing to connect to. -1. Download the 1.16.5 **Minecraft Forge** installer from the link above, making sure to download the most recent recommended version. - -2. Run the `forge-1.16.5-xx.x.x-installer.jar` file and choose **install server**. - - On this page you will also choose where to install the server to remember this directory it's important in the next step. - -3. Navigate to where you installed the server and open `forge-1.16.5-xx.x.x.jar` - - Upon first launch of the server it will close and ask you to accept Minecraft's EULA. There will be a new file called `eula.txt` that contains a link to Minecraft's EULA, and a line that you need to change to `eula=true` to accept Minecraft's EULA. - - This will create the appropriate directories for you to place the files in the following step. - -4. Place the `aprandomizer-x.x.x.jar` from the link above file into the `mods` folder of the above installation of your forge server. - - Once again run the server, it will load up and generate the required directory `APData` for when you are ready to play a game! diff --git a/WebHostLib/static/assets/tutorial/zelda3/msu1_en.md b/WebHostLib/static/assets/tutorial/zelda3/msu1_en.md index d6d30d7c..f749f4a9 100644 --- a/WebHostLib/static/assets/tutorial/zelda3/msu1_en.md +++ b/WebHostLib/static/assets/tutorial/zelda3/msu1_en.md @@ -74,3 +74,4 @@ Below is a list of MSU packs which, so far as we know, are safe to stream. More we learn of them. If you know of any we missed, please let us know! - Vanilla Game Music - [Smooth McGroove](https://drive.google.com/open?id=1JDa1jCKg5hG0Km6xNpmIgf4kDMOxVp3n) +- Zelda community member Amarith assembled the following list for the purpose of competitive restreams. While we have not ourselves verified this list, all submissions required VoD proof they were not muted. Generally speaking, MSU-1 packs are less safe if they contain lyrics at any point. This list was only tested on Twitch and results for other platforms may vary. [Restream-Safe List](https://tinyurl.com/MSUsApprovedForLeagueChannels) diff --git a/WebHostLib/static/assets/tutorial/zelda3/multiworld_en.md b/WebHostLib/static/assets/tutorial/zelda3/multiworld_en.md index 7b2c2689..d0cd9547 100644 --- a/WebHostLib/static/assets/tutorial/zelda3/multiworld_en.md +++ b/WebHostLib/static/assets/tutorial/zelda3/multiworld_en.md @@ -67,7 +67,7 @@ When the client launched automatically, SNI should have also automatically launc 4. In the new window, click **Browse...** 5. Select the connector lua file included with your client - Z3Client users should download `sniConnector.lua` from the client download page - - SNIClient users should look in their Archipelago folder for `/sni/Connector.lua` + - SNIClient users should look in their Archipelago folder for `/sni/lua` ##### BizHawk 1. Ensure you have the BSNES core loaded. You may do this by clicking on the Tools menu in BizHawk and following @@ -76,10 +76,11 @@ When the client launched automatically, SNI should have also automatically launc Once you have changed the loaded core, you must restart BizHawk. 2. Load your ROM file if it hasn't already been loaded. 3. Click on the Tools menu and click on **Lua Console** -4. Click the button to open a new Lua script. -5. Select the `sniConnector.lua` file you downloaded above +4. Click Script -> Open Script... +5. Select the `Connector.lua` file you downloaded above - Z3Client users should download `sniConnector.lua` from the client download page - - SNIClient users should look in their Archipelago folder for `/sni/Connector.lua` + - SNIClient users should look in their Archipelago folder for `/sni/lua` +6. Run the script by double-clicking it in the listing #### With hardware This guide assumes you have downloaded the correct firmware for your device. If you have not done so already, please do this now. SD2SNES and FXPak Pro users may download the appropriate firmware [from the sd2snes releases page](https://github.com/RedGuyyyy/sd2snes/releases). Other hardware may find helpful information [on the usb2snes supported platforms page](http://usb2snes.com/#supported-platforms). diff --git a/WebHostLib/static/static/weightedSettings.json b/WebHostLib/static/static/weightedSettings.json deleted file mode 100644 index ba12ac7c..00000000 --- a/WebHostLib/static/static/weightedSettings.json +++ /dev/null @@ -1,2146 +0,0 @@ -{ - "gameOptions": { - "description": { - "keyString": "description", - "friendlyName": "Description", - "inputType": "text", - "description": "A short description of this preset. Useful if you have multiple files", - "defaultValue": "Preset Name" - }, - "name": { - "keyString": "name", - "friendlyName": "Player Name", - "inputType": "text", - "description": "Displayed in-game. Spaces will be replaced with underscores.", - "defaultValue": "Your Name" - }, - "glitches_required": { - "keyString": "glitches_required", - "friendlyName": "Glitches Required", - "description": "Determine the logic required to complete the seed.", - "inputType": "range", - "subOptions": { - "none": { - "keyString": "glitches_required.none", - "friendlyName": "None", - "description": "No glitches required.", - "defaultValue": 50 - }, - "minor_glitches": { - "keyString": "glitches_required.minor_glitches", - "friendlyName": "Minor Glitches", - "description": "Puts fake flipper, water-walk, super bunny, etc into logic", - "defaultValue": 0 - }, - "overworld_glitches": { - "keyString": "glitches_required.overworld_glitches", - "friendlyName": "Overworld Glitches", - "description": "Assumes the player has knowledge of both overworld major glitches (boots clips, mirror clips) and minor glitches (fake flipper, super bunny shenanigans, water walk and etc.)", - "defaultValue": 0 - }, - "no_logic": { - "keyString": "glitches_required.no_logic", - "friendlyName": "No Logic", - "description": "Your items are placed with no regard to any logic. Your Fire Rod could be on your Trinexx.", - "defaultValue": 0 - } - } - }, - "dark_room_logic": { - "keyString": "dark_room_logic", - "friendlyName": "Dark Room Logic", - "description": "Logic to use for dark rooms.", - "inputType": "range", - "subOptions": { - "lamp": { - "keyString": "dark_room_logic.lamp", - "friendlyName": "Lamp Required", - "description": "The lamp is required for dark rooms to be considered in logic.", - "defaultValue": 50 - }, - "torches": { - "keyString": "dark_room_logic.torches", - "friendlyName": "Lamp or Torches", - "description": "In addition to the lamp, a fire rod and accessible torches may put dark rooms into logic.", - "defaultValue": 0 - }, - "none": { - "keyString": "dark_room_logic.none", - "friendlyName": "Always in Logic", - "description": "Dark rooms are always considered in logic, which may require you to navigate rooms in complete darkness.", - "defaultValue": 0 - } - } - }, - "restrict_dungeon_item_on_boss": { - "keyString": "restrict_dungeon_item_on_boss", - "friendlyName": "Dungeon Item on Boss", - "description": "Prevent dungeon bosses from dropping maps, compasses, and keys.", - "inputType": "range", - "subOptions": { - "on": { - "keyString": "restrict_dungeon_item_on_boss.on", - "friendlyName": "On", - "description": "Dungeon bosses will never drop maps, compasses, or keys.", - "defaultValue": 0 - }, - "off": { - "keyString": "restrict_dungeon_item_on_boss.off", - "friendlyName": "Off", - "description": "Dungeon bosses may drop any item.", - "defaultValue": 0 - } - } - }, - "map_shuffle": { - "keyString": "map_shuffle", - "friendlyName": "Map Shuffle", - "description": "Shuffle dungeon maps into the world and other dungeons, including other players' worlds.", - "inputType": "range", - "subOptions": { - "off": { - "keyString": "map_shuffle.off", - "friendlyName": "Off", - "description": "Disable map shuffle.", - "defaultValue": 50 - }, - "on": { - "keyString": "map_shuffle.on", - "friendlyName": "On", - "description": "Enable map shuffle.", - "defaultValue": 0 - } - } - }, - "compass_shuffle": { - "keyString": "compass_shuffle", - "friendlyName": "Compass Shuffle", - "description": "Shuffle compasses into the world and other dungeons, including other players' worlds", - "inputType": "range", - "subOptions": { - "off": { - "keyString": "compass_shuffle.off", - "friendlyName": "Off", - "description": "Disable compass shuffle.", - "defaultValue": 50 - }, - "on": { - "keyString": "compass_shuffle.on", - "friendlyName": "On", - "description": "Enable compass shuffle.", - "defaultValue": 0 - } - } - }, - "smallkey_shuffle": { - "keyString": "smallkey_shuffle", - "friendlyName": "Small Key Shuffle", - "description": "Shuffle small keys into the world and other dungeons, including other players' worlds.", - "inputType": "range", - "subOptions": { - "on": { - "keyString": "smallkey_shuffle.on", - "friendlyName": "On", - "description": "Enable small key shuffle.", - "defaultValue": 0 - }, - "off": { - "keyString": "smallkey_shuffle.off", - "friendlyName": "Off", - "description": "Disable small key shuffle.", - "defaultValue": 50 - }, - "universal": { - "keyString": "smallkey_shuffle.universal", - "friendlyName": "Universal", - "description": "Allows small keys to be used in any dungeon and adds keys to shops so you can buy more.", - "defaultValue": 0 - } - } - }, - "bigkey_shuffle": { - "keyString": "bigkey_shuffle", - "friendlyName": "Big Key Shuffle", - "description": "Shuffle big keys into the world and other dungeons, including other players' worlds.", - "inputType": "range", - "subOptions": { - "on": { - "keyString": "bigkey_shuffle.on", - "friendlyName": "On", - "description": "Enable big key shuffle.", - "defaultValue": 0 - }, - "off": { - "keyString": "bigkey_shuffle.off", - "friendlyName": "Off", - "description": "Disable big key shuffle.", - "defaultValue": 50 - } - } - }, - "local_keys": { - "keyString": "local_keys", - "friendlyName": "Local Keys", - "description": "Keep small keys and big keys local to your world.", - "inputType": "range", - "subOptions": { - "on": { - "keyString": "local_keys.on", - "friendlyName": "On", - "description": "Enable local keys.", - "defaultValue": 0 - }, - "off": { - "keyString": "local_keys.off", - "friendlyName": "Off", - "description": "Disable local keys.", - "defaultValue": 50 - } - } - }, - "dungeon_counters": { - "keyString": "dungeon_counters", - "friendlyName": "Dungeon Counters", - "description": "Determines when to show an on-screen counter for dungeon items.", - "inputType": "range", - "subOptions": { - "on": { - "keyString": "dungeon_counters.on", - "friendlyName": "Always On", - "description": "Always display amount of items checked in a dungeon.", - "defaultValue": 0 - }, - "pickup": { - "keyString": "dungeon_counters.pickup", - "friendlyName": "With Compass", - "description": "Show when compass is picked up.", - "defaultValue": 0 - }, - "default": { - "keyString": "dungeon_counters.default", - "friendlyName": "With Compass if Shuffled", - "description": "Show when the compass is picked up, if the compass was shuffled.", - "defaultValue": 0 - }, - "off": { - "keyString": "dungeon_counters.off", - "friendlyName": "Always Off", - "description": "Never show dungeon counters.", - "defaultValue": 50 - } - } - }, - "accessibility": { - "keyString": "accessibility", - "friendlyName": "Location Access", - "description": "Determines how much of the game is guaranteed to be reachable.", - "inputType": "range", - "subOptions": { - "items": { - "keyString": "accessibility.items", - "friendlyName": "All Items", - "description": "Guarantees you will be able to acquire all items, but you may not be able to access all locations.", - "defaultValue": 0 - }, - "locations": { - "keyString": "accessibility.locations", - "friendlyName": "All Locations", - "description": "Guarantees you will be able to access all locations, and therefore all items.", - "defaultValue": 50 - }, - "none": { - "keyString": "accessibility.none", - "friendlyName": "Required Only", - "description": "Guarantees only that the game is beatable. You may not be able to access all locations or acquire all items.", - "defaultValue": 0 - } - } - }, - "progressive": { - "keyString": "progressive", - "friendlyName": "Progressive Items", - "description": "Enable or disable the progressive acquisition of certain items (swords, shields, bow).", - "inputType": "range", - "subOptions": { - "on": { - "keyString": "progressive.on", - "friendlyName": "On", - "description": "All relevant items are acquired progressively.", - "defaultValue": 50 - }, - "off": { - "keyString": "progressive.off", - "friendlyName": "Off", - "description": "All relevant items are acquired non-progressively (tempered sword may be in Link's House).", - "defaultValue": 0 - }, - "random": { - "keyString": "progressive.random", - "friendlyName": "Random", - "description": "The progressive nature of items is determined per-item pool. Gloves may be progressive, but swords may not be.", - "defaultValue": 0 - } - } - }, - "entrance_shuffle": { - "keyString": "entrance_shuffle", - "friendlyName": "Entrance Shuffle", - "description": "Determines how often and by what rules entrances are shuffled.", - "inputType": "range", - "subOptions": { - "none": { - "keyString": "entrance_shuffle.none", - "friendlyName": "Vanilla Entrances", - "description": "Vanilla game map. All entrances and exits lead to their original locations.", - "defaultValue": 50 - }, - "dungeonssimple": { - "keyString": "entrance_shuffle.dungeonssimple", - "friendlyName": "Dungeons Simple", - "description": "Shuffle whole dungeons amongst each other. Hyrule Castle would always be one dungeon.", - "defaultValue": 0 - }, - "dungeonsfull": { - "keyString": "entrance_shuffle.dungeonsfull", - "friendlyName": "Dungeons Full", - "description": "Shuffle any dungeon entrance with any dungeon interior, so Hyrule Castle could be four different dungeons.", - "defaultValue": 0 - }, - "simple": { - "keyString": "entrance_shuffle.simple", - "friendlyName": "Simple Shuffle", - "description": "Entrances are grouped together before being randomized. This option uses the most strict grouping rules.", - "defaultValue": 0 - }, - "restricted": { - "keyString": "entrance_shuffle.restricted", - "friendlyName": "Restricted Shuffle", - "description": "Entrances are grouped together before being randomized. Grouping rules are less strict than Simple Shuffle.", - "defaultValue": 0 - }, - "full": { - "keyString": "entrance_shuffle.full", - "friendlyName": "Full Shuffle", - "description": "Entrances are grouped before being randomized. Grouping rules are less strict than Restricted Shuffle.", - "defaultValue": 0 - }, - "crossed": { - "keyString": "entrance_shuffle.crossed", - "friendlyName": "Crossed Shuffle", - "description": "Entrances are grouped before being randomized. Grouping rules are less strict than Full Shuffle.", - "defaultValue": 0 - }, - "insanity": { - "keyString": "entrance_shuffle.insanity", - "friendlyName": "Insanity Shuffle", - "description": "Very few entrance grouping rules are applied. Good luck.", - "defaultValue": 0 - } - } - }, - "goals": { - "keyString": "goals", - "friendlyName": "Goals", - "description": "Determines how much work you need to put in to save Hyrule.", - "inputType": "range", - "subOptions": { - "ganon": { - "keyString": "goals.ganon", - "friendlyName": "Defeat Ganon", - "description": "Climb Ganon's Tower, defeat Agahnim, then defeat Ganon in his lair.", - "defaultValue": 50 - }, - "fast_ganon": { - "keyString": "goals.fast_ganon", - "friendlyName": "Fast Ganon", - "description": "Kill Ganon in his lair. The hole is always open, but you may still require some crystals to damage him.", - "defaultValue": 0 - }, - "bosses": { - "keyString": "goals.bosses", - "friendlyName": "All Bosses", - "description": "Defeat the boss of all dungeons, defeat Agahnim in both Castle Tower and Ganon's Tower, then defeat Ganon in his lair.", - "defaultValue": 0 - }, - "pedestal": { - "keyString": "goals.pedestal", - "friendlyName": "Pedestal", - "description": "Acquire all three pendants and pull the Triforce from the Master Sword Pedestal.", - "defaultValue": 0 - }, - "ganon_pedestal": { - "keyString": "goals.ganon_pedestal", - "friendlyName": "Ganon Pedestal", - "description": "Accquire all three pendants, pull the Master Sword Pedestal, then defeat Ganon in his lair.", - "defaultValue": 0 - }, - "triforce_hunt": { - "keyString": "goals.triforce_hunt", - "friendlyName": "Triforce Hunt", - "description": "Collect enough pieces of the Triforce of Courage, which has been spread around the world, then turn them in to Murahadala, who is standing outside Hyrule Castle.", - "defaultValue": 0 - }, - "local_triforce_hunt": { - "keyString": "goals.local_triforce_hunt", - "friendlyName": "Local Triforce Hunt", - "description": "Same as Triforce Hunt, but the Triforce pieces are guaranteed to be in your world.", - "defaultValue": 0 - }, - "ganon_triforce_hunt": { - "keyString": "goals.ganon_triforce_hunt", - "friendlyName": "Triforce Hunt /w Ganon", - "description": "Same as Triforce Hunt, but you need to defeat Ganon in his lair instead of talking with Murahadala.", - "defaultValue": 0 - }, - "local_ganon_triforce_hunt": { - "keyString": "goals.local_ganon_triforce_hunt", - "friendlyName": "Local Triforce hunt /w Ganon", - "description": "Same as Local Triforce Hunt, but you need to defeat Ganon in his lair instead of talking with Murahadala.", - "defaultValue": 0 - }, - "ice_rod_hunt": { - "keyString": "goals.ice_rod_hunt", - "friendlyName": "Ice Rod Hunt", - "description": "Look for the Ice Rod within your 215 available checks, then go kill Trinexx at Turtle rock.", - "defaultValue": 0 - } - } - }, - "pyramid_open": { - "keyString": "pyramid_open", - "friendlyName": "Pyramid Open", - "description": "", - "inputType": "range", - "subOptions": { - "goal": { - "keyString": "pyramid_open.goal", - "friendlyName": "Goal", - "description": "Opens the pyramid if the goal requires you to kill Ganon, unless the goal is Slow Ganon or All Dungeons.", - "defaultValue": 50 - }, - "auto": { - "keyString": "pyramid_open.auto", - "friendlyName": "Auto", - "description": "Same as Goal, but also opens when any non-dungeon entrance shuffle is used.", - "defaultValue": 0 - }, - "yes": { - "keyString": "pyramid_open.yes", - "friendlyName": "Always Open", - "description": "Pyramid hole is always open. Ganon's vulnerable condition is still required before he can he hurt.", - "defaultValue": 0 - }, - "no": { - "keyString": "pyramid_open.no", - "friendlyName": "Always Closed", - "description": "Pyramid hole is always closed until you defeat Agahnim atop Ganon's Tower.", - "defaultValue": 0 - } - } - }, - "triforce_pieces_required": { - "keyString": "triforce_pieces_required", - "friendlyName": "Triforce Pieces Required", - "description": "Determines the total number of Triforce pieces required before speaking with Murahadala", - "inputType": "range", - "subOptions": { - "15": { - "keyString": "triforce_pieces_required.15", - "friendlyName": 15, - "description": "15 Triforce pieces are required before speaking with Murahadala.", - "defaultValue": 0 - }, - "20": { - "keyString": "triforce_pieces_required.20", - "friendlyName": 20, - "description": "20 Triforce pieces are required before speaking with Murahadala.", - "defaultValue": 50 - }, - "30": { - "keyString": "triforce_pieces_required.30", - "friendlyName": 30, - "description": "30 Triforce pieces are required before speaking with Murahadala.", - "defaultValue": 0 - }, - "40": { - "keyString": "triforce_pieces_required.40", - "friendlyName": 40, - "description": "40 Triforce pieces are required before speaking with Murahadala.", - "defaultValue": 0 - }, - "50": { - "keyString": "triforce_pieces_required.50", - "friendlyName": 50, - "description": "50 Triforce pieces are required before speaking with Murahadala.", - "defaultValue": 0 - } - } - }, - "triforce_pieces_mode": { - "keyString": "triforce_pieces_mode", - "friendlyName": "Triforce Piece Availability Mode", - "description": "Determines which of the following three options will be used to determine the total available triforce pieces.", - "inputType": "range", - "subOptions": { - "available": { - "keyString": "triforce_pieces_mode.available", - "friendlyName": "Exact Number", - "description": "Explicitly tell the generator how many triforce pieces to place throughout Hyrule.", - "defaultValue": 50 - }, - "extra": { - "keyString": "triforce_pieces_mode.extra", - "friendlyName": "Required Plus", - "description": "Set the number of triforce pieces in Hyrule equal to the number of required pieces plus a number specified by this option.", - "defaultValue": 0 - }, - "percentage": { - "keyString": "triforce_pieces_mode.percentage", - "friendlyName": "Percentage", - "description": "Set the number of triforce pieces in Hyrule equal to the number of required pieces plus a percentage specified by this option.", - "defaultValue": 0 - } - } - }, - "triforce_pieces_available": { - "keyString": "triforce_pieces_available", - "friendlyName": "Exact Number (Triforce Hunt)", - "description": "Only used if enabled in Triforce Piece Availability Mode.", - "inputType": "range", - "subOptions": { - "25": { - "keyString": "triforce_pieces_available.25", - "friendlyName": 25, - "description": "25 Triforce pieces will be hidden throughout Hyrule", - "defaultValue": 0 - }, - "30": { - "keyString": "triforce_pieces_available.30", - "friendlyName": 30, - "description": "30 Triforce pieces will be hidden throughout Hyrule", - "defaultValue": 50 - }, - "40": { - "keyString": "triforce_pieces_available.40", - "friendlyName": 40, - "description": "40 Triforce pieces will be hidden throughout Hyrule", - "defaultValue": 0 - }, - "50": { - "keyString": "triforce_pieces_available.50", - "friendlyName": 50, - "description": "50 Triforce pieces will be hidden throughout Hyrule", - "defaultValue": 0 - } - } - }, - "triforce_pieces_extra": { - "keyString": "triforce_pieces_extra", - "friendlyName": "Required Plus (Triforce Hunt)", - "description": "Only used if enabled in Triforce Piece Availability Mode.", - "inputType": "range", - "subOptions": { - "0": { - "keyString": "triforce_pieces_extra.0", - "friendlyName": 0, - "description": "No extra Triforce pieces will be hidden throughout Hyrule", - "defaultValue": 0 - }, - "5": { - "keyString": "triforce_pieces_extra.5", - "friendlyName": 5, - "description": "5 extra Triforce pieces will be hidden throughout Hyrule", - "defaultValue": 0 - }, - "10": { - "keyString": "triforce_pieces_extra.10", - "friendlyName": 10, - "description": "10 extra Triforce pieces will be hidden throughout Hyrule", - "defaultValue": 50 - }, - "15": { - "keyString": "triforce_pieces_extra.15", - "friendlyName": 15, - "description": "15 extra Triforce pieces will be hidden throughout Hyrule", - "defaultValue": 0 - }, - "20": { - "keyString": "triforce_pieces_extra.20", - "friendlyName": 20, - "description": "20 extra Triforce pieces will be hidden throughout Hyrule", - "defaultValue": 0 - } - } - }, - "triforce_pieces_percentage": { - "keyString": "triforce_pieces_percentage", - "friendlyName": "Percentage (Triforce Hunt)", - "description": "Only used if enabled in Triforce Piece Availability Mode.", - "inputType": "range", - "subOptions": { - "100": { - "keyString": "triforce_pieces_percentage.100", - "friendlyName": "0%", - "description": "No extra Triforce pieces will be hidden throughout Hyrule", - "defaultValue": 0 - }, - "150": { - "keyString": "triforce_pieces_percentage.150", - "friendlyName": "50%", - "description": "50% more triforce pieces than required will be placed throughout Hyrule.", - "defaultValue": 50 - }, - "200": { - "keyString": "triforce_pieces_percentage.200", - "friendlyName": "100%", - "description": "50% more triforce pieces than required will be placed throughout Hyrule.", - "defaultValue": 0 - } - } - }, - "tower_open": { - "keyString": "tower_open", - "friendlyName": "GT Crystals", - "description": "Determines the number of crystals required to open Ganon's Tower.", - "inputType": "range", - "subOptions": { - "0": { - "keyString": "tower_open.0", - "friendlyName": 0, - "description": "0 Crystals are required to open Ganon's Tower.", - "defaultValue": 80 - }, - "1": { - "keyString": "tower_open.1", - "friendlyName": 1, - "description": "1 Crystal is required to open Ganon's Tower.", - "defaultValue": 70 - }, - "2": { - "keyString": "tower_open.2", - "friendlyName": 2, - "description": "2 Crystals are required to open Ganon's Tower.", - "defaultValue": 60 - }, - "3": { - "keyString": "tower_open.3", - "friendlyName": 3, - "description": "3 Crystals are required to open Ganon's Tower.", - "defaultValue": 50 - }, - "4": { - "keyString": "tower_open.4", - "friendlyName": 4, - "description": "4 Crystals are required to open Ganon's Tower.", - "defaultValue": 40 - }, - "5": { - "keyString": "tower_open.5", - "friendlyName": 5, - "description": "5 Crystals are required to open Ganon's Tower.", - "defaultValue": 30 - }, - "6": { - "keyString": "tower_open.6", - "friendlyName": 6, - "description": "6 Crystals are required to open Ganon's Tower.", - "defaultValue": 20 - }, - "7": { - "keyString": "tower_open.7", - "friendlyName": 7, - "description": "7 Crystals are required to open Ganon's Tower.", - "defaultValue": 10 - }, - "random": { - "keyString": "tower_open.random", - "friendlyName": "Random", - "description": "Randomly determine the number of crystals necessary to open Ganon's Tower.", - "defaultValue": 0 - } - } - }, - "ganon_open": { - "keyString": "ganon_open", - "friendlyName": "Ganon Crystals", - "description": "Determines the number of crystals required before you are able to damage Ganon.", - "inputType": "range", - "subOptions": { - "0": { - "keyString": "ganon_open.0", - "friendlyName": 0, - "description": "0 Crystals are required to damage Ganon.", - "defaultValue": 80 - }, - "1": { - "keyString": "ganon_open.1", - "friendlyName": 1, - "description": "1 Crystal is required to damage Ganon.", - "defaultValue": 70 - }, - "2": { - "keyString": "ganon_open.2", - "friendlyName": 2, - "description": "2 Crystals are required to damage Ganon.", - "defaultValue": 60 - }, - "3": { - "keyString": "ganon_open.3", - "friendlyName": 3, - "description": "3 Crystals are required to damage Ganon.", - "defaultValue": 50 - }, - "4": { - "keyString": "ganon_open.4", - "friendlyName": 4, - "description": "4 Crystals are required to damage Ganon.", - "defaultValue": 40 - }, - "5": { - "keyString": "ganon_open.5", - "friendlyName": 5, - "description": "5 Crystals are required to damage Ganon.", - "defaultValue": 30 - }, - "6": { - "keyString": "ganon_open.6", - "friendlyName": 6, - "description": "6 Crystals are required to damage Ganon.", - "defaultValue": 20 - }, - "7": { - "keyString": "ganon_open.7", - "friendlyName": 7, - "description": "7 Crystals are required to damage Ganon.", - "defaultValue": 10 - }, - "random": { - "keyString": "ganon_open.random", - "friendlyName": "Random", - "description": "Randomly determine the number of crystals necessary to damage Ganon.", - "defaultValue": 0 - } - } - }, - "mode": { - "keyString": "mode", - "friendlyName": "Game Mode", - "description": "Determines the mode, or world state, for your game.", - "inputType": "range", - "subOptions": { - "standard": { - "keyString": "mode.standard", - "friendlyName": "Standard Mode", - "description": "Begin the game by rescuing Zelda from her cell and escorting her to the Sanctuary.", - "defaultValue": 50 - }, - "open": { - "keyString": "mode.open", - "friendlyName": "Open Mode", - "description": "Begin the game from your choice of Link's House or the Sanctuary.", - "defaultValue": 50 - }, - "inverted": { - "keyString": "mode.inverted", - "friendlyName": "Inverted Mode", - "description": "Begin in the Dark World. The Moon Pearl is required to avoid bunny-state in Light World, and the Light World game map is altered.", - "defaultValue": 0 - } - } - }, - "retro": { - "keyString": "retro", - "friendlyName": "Retro Mode", - "description": "Makes the game similar to the first Legend of Zelda. You must buy a quiver to use the bow, take-any caves and an old-man cave are added to the world, and you may need to find your sword from the old man's cave.", - "inputType": "range", - "subOptions": { - "on": { - "keyString": "retro.on", - "friendlyName": "On", - "description": "Enable retro mode.", - "defaultValue": 0 - }, - "off": { - "keyString": "retro.off", - "friendlyName": "Off", - "description": "Disable retro mode.", - "defaultValue": 50 - } - } - }, - "hints": { - "keyString": "hints", - "friendlyName": "Hint Type", - "description": "Determines the behavior of hint tiles in dungeons", - "inputType": "range", - "subOptions": { - "on": { - "keyString": "hints.on", - "friendlyName": "Item Locations", - "description": "Hint tiles sometimes give item location hints.", - "defaultValue": 50 - }, - "off": { - "keyString": "hints.off", - "friendlyName": "Gameplay Tips", - "description": "Hint tiles provide gameplay tips.", - "defaultValue": 0 - } - } - }, - "weapons": { - "keyString": "weapons", - "friendlyName": "Sword Placement", - "description": "Determines how swords are placed throughout the world.", - "inputType": "range", - "subOptions": { - "randomized": { - "keyString": "weapons.randomized", - "friendlyName": "Randomized", - "description": "Swords are placed randomly throughout the world.", - "defaultValue": 0 - }, - "assured": { - "keyString": "weapons.assured", - "friendlyName": "Assured", - "description": "Begin the game with a sword. Other swords are placed randomly throughout the game world.", - "defaultValue": 50 - }, - "vanilla": { - "keyString": "weapons.vanilla", - "friendlyName": "Vanilla Locations", - "description": "Swords are placed in vanilla locations in your own game (uncle, pedestal, smiths, pyramid fairy).", - "defaultValue": 0 - }, - "swordless": { - "keyString": "weapons.swordless", - "friendlyName": "Swordless", - "description": "Your swords are replaced with rupees. Gameplay changes are made to accommodate this change.", - "defaultValue": 0 - } - } - }, - "item_pool": { - "keyString": "item_pool", - "friendlyName": "Item Pool", - "description": "Determines the availability of upgrades, progressive items, and convenience items.", - "inputType": "range", - "subOptions": { - "easy": { - "keyString": "item_pool.easy", - "friendlyName": "Easy", - "description": "Double the number of available upgrades and progressive items.", - "defaultValue": 0 - }, - "normal": { - "keyString": "item_pool.normal", - "friendlyName": "Normal", - "description": "Item availability remains unchanged from the vanilla game.", - "defaultValue": 50 - }, - "hard": { - "keyString": "item_pool.hard", - "friendlyName": "Hard", - "description": "Reduced upgrade availability (max: 14 hearts, blue mail, tempered sword, fire shield, no silvers unless swordless).", - "defaultValue": 0 - }, - "expert": { - "keyString": "item_pool.expert", - "friendlyName": "Expert", - "description": "Minimum upgrade availability (max: 8 hearts, green mail, master sword, fighter shield, no silvers unless swordless).", - "defaultValue": 0 - } - } - }, - "item_functionality": { - "keyString": "item_functionality", - "friendlyName": "Item Functionality", - "description": "Alters the usefulness of various items in the game.", - "inputType": "range", - "subOptions": { - "easy": { - "keyString": "item_functionality.easy", - "friendlyName": "Easy", - "description": "Increases helpfulness of items. Medallions are usable everywhere, even without a sword. Hammer can be used in place of master sword to beat ganon and collect the tablets.", - "defaultValue": 0 - }, - "normal": { - "keyString": "item_functionality.normal", - "friendlyName": "Normal", - "description": "Item functionality remains unchanged from the vanilla game.", - "defaultValue": 50 - }, - "hard": { - "keyString": "item_functionality.hard", - "friendlyName": "Hard", - "description": "Reduced helpfulness of items. Potions are less effective, you can't catch faeries, the Magic Cape uses double magic, the Cane of Byrna does not grant invulnerability, boomerangs do not stun, and silver arrows are disabled outside ganon.", - "defaultValue": 0 - }, - "expert": { - "keyString": "item_functionality.expert", - "friendlyName": "Expert", - "description": "Vastly reduces the helpfulness of items. Potions are barely effective, you can't catch faeries, the Magic Cape uses double magic, the Cane of Byrna does not grant invulnerability, boomerangs and hookshot do not stun, and the silver arrows are disabled outside ganon.", - "defaultValue": 0 - } - } - }, - "progression_balancing": { - "keyString": "progression_balancing", - "friendlyName": "Progression Balancing", - "description": "A system to reduce time spent in BK mode. It moves your items into an earlier access sphere to make it more likely you have access to progression items.", - "inputType": "range", - "subOptions": { - "on": { - "keyString": "progression_balancing.on", - "friendlyName": "On", - "description": "Enable progression balancing.", - "defaultValue": 50 - }, - "off": { - "keyString": "progression_balancing.off", - "friendlyName": "Off", - "description": "Disable progression balancing.", - "defaultValue": 0 - } - } - }, - "boss_shuffle": { - "keyString": "boss_shuffle", - "friendlyName": "Boss Shuffle", - "description": "Determines which boss appears in which dungeon.", - "inputType": "range", - "subOptions": { - "none": { - "keyString": "boss_shuffle.none", - "friendlyName": "None", - "description": "Bosses appear in vanilla locations.", - "defaultValue": 50 - }, - "simple": { - "keyString": "boss_shuffle.simple", - "friendlyName": "Simple", - "description": "Existing bosses except Ganon and Agahnim are shuffled throughout dungeons.", - "defaultValue": 0 - }, - "full": { - "keyString": "boss_shuffle.full", - "friendlyName": "Full", - "description": "Bosses are shuffled, and three of them may occur twice.", - "defaultValue": 0 - }, - "random": { - "keyString": "boss_shuffle.random", - "friendlyName": "Random", - "description": "Any boss may appear any number of times.", - "defaultValue": 0 - }, - "singularity": { - "keyString": "boss_shuffle.singularity", - "friendlyName": "Singularity", - "description": "Picks a boss at random and puts it in every dungeon it can appear in. Remaining dungeons bosses are chosen at random.", - "defaultValue": 0 - } - } - }, - "enemy_shuffle": { - "keyString": "enemy_shuffle", - "friendlyName": "Enemy Shuffle", - "description": "Randomizes which enemies appear throughout the game.", - "inputType": "range", - "subOptions": { - "on": { - "keyString": "enemy_shuffle.on", - "friendlyName": "On", - "description": "Enable enemy shuffle.", - "defaultValue": 0 - }, - "off": { - "keyString": "enemy_shuffle.off", - "friendlyName": "Off", - "description": "Disable enemy shuffle.", - "defaultValue": 50 - } - } - }, - "killable_thieves": { - "keyString": "killable_thieves", - "friendlyName": "Killable Thieves", - "description": "Determines whether thieves may be killed or not.", - "inputType": "range", - "subOptions": { - "on": { - "keyString": "killable_thieves.on", - "friendlyName": "On", - "description": "Thieves are mortal.", - "defaultValue": 0 - }, - "off": { - "keyString": "killable_thieves.off", - "friendlyName": "Off", - "description": "Thieves are invulnerable.", - "defaultValue": 50 - } - } - }, - "tile_shuffle": { - "keyString": "tile_shuffle", - "friendlyName": "Tile Shuffle", - "description": "Randomizes tile layouts in rooms where floor tiles attack you.", - "inputType": "range", - "subOptions": { - "on": { - "keyString": "tile_shuffle.on", - "friendlyName": "On", - "description": "Enable tile shuffle.", - "defaultValue": 0 - }, - "off": { - "keyString": "tile_shuffle.off", - "friendlyName": "Off", - "description": "Disable tile shuffle.", - "defaultValue": 50 - } - } - }, - "misery_mire_medallion": { - "keyString": "misery_mire_medallion", - "friendlyName": "Misery Mire Medallion", - "description": "Determines the medallion required to access Misery Mire", - "inputType": "range", - "subOptions": { - "random": { - "keyString": "misery_mire_medallion.random", - "friendlyName": "Random", - "description": "Choose the medallion randomly.", - "defaultValue": 50 - }, - "quake": { - "keyString": "misery_mire_medallion.quake", - "friendlyName": "Quake", - "description": "Quake will be required ot enter Misery Mire.", - "defaultValue": 0 - }, - "bombos": { - "keyString": "misery_mire_medallion.bombos", - "friendlyName": "Bombos", - "description": "Bombos will be required ot enter Misery Mire.", - "defaultValue": 0 - }, - "ether": { - "keyString": "misery_mire_medallion.ether", - "friendlyName": "Ether", - "description": "Ether will be required ot enter Misery Mire.", - "defaultValue": 0 - } - } - }, - "turtle_rock_medallion": { - "keyString": "turtle_rock_medallion", - "friendlyName": "Turtle Rock Medallion", - "description": "Determines the medallion required to access Turtle Rock", - "inputType": "range", - "subOptions": { - "random": { - "keyString": "turtle_rock_medallion.random", - "friendlyName": "Random", - "description": "Choose the medallion randomly.", - "defaultValue": 50 - }, - "quake": { - "keyString": "turtle_rock_medallion.quake", - "friendlyName": "Quake", - "description": "Quake will be required ot enter Turtle Rock.", - "defaultValue": 0 - }, - "bombos": { - "keyString": "turtle_rock_medallion.bombos", - "friendlyName": "Bombos", - "description": "Bombos will be required ot enter Turtle Rock.", - "defaultValue": 0 - }, - "ether": { - "keyString": "turtle_rock_medallion.ether", - "friendlyName": "Ether", - "description": "Ether will be required ot enter Turtle Rock.", - "defaultValue": 0 - } - } - }, - "bush_shuffle": { - "keyString": "bush_shuffle", - "friendlyName": "Bush Shuffle", - "description": "Randomize the chance that bushes around Hyrule have enemies hiding under them.", - "inputType": "range", - "subOptions": { - "on": { - "keyString": "bush_shuffle.on", - "friendlyName": "On", - "description": "Enable bush shuffle.", - "defaultValue": 0 - }, - "off": { - "keyString": "bush_shuffle.off", - "friendlyName": "Off", - "description": "Disable bush shuffle.", - "defaultValue": 50 - } - } - }, - "enemy_damage": { - "keyString": "enemy_damage", - "friendlyName": "Enemy Damage", - "description": "Randomizes how much damage enemies can deal to you.", - "inputType": "range", - "subOptions": { - "default": { - "keyString": "enemy_damage.default", - "friendlyName": "Vanilla Damage", - "description": "Enemies deal the same damage as in the vanilla game.", - "defaultValue": 50 - }, - "shuffled": { - "keyString": "enemy_damage.shuffled", - "friendlyName": "Shuffled", - "description": "Enemies deal zero to four hearts of damage, and armor reduces this damage.", - "defaultValue": 0 - }, - "random": { - "keyString": "enemy_damage.random", - "friendlyName": "Random", - "description": "Enemies may deal zero through eight hearts of damage, and armor re-shuffles how much damage you take from each enemy.", - "defaultValue": 0 - } - } - }, - "enemy_health": { - "keyString": "enemy_health", - "friendlyName": "Enemy Health", - "description": "Randomizes the amount of health enemies have. Does not affect bosses.", - "inputType": "range", - "subOptions": { - "default": { - "keyString": "enemy_health.default", - "friendlyName": "Vanilla", - "description": "Enemies have the same amount of health as in the vanilla game.", - "defaultValue": 50 - }, - "easy": { - "keyString": "enemy_health.easy", - "friendlyName": "Reduced", - "description": "Enemies have generally reduced health.", - "defaultValue": 0 - }, - "hard": { - "keyString": "enemy_health.hard", - "friendlyName": "Increased", - "description": "Enemies have generally increased health.", - "defaultValue": 0 - }, - "expert": { - "keyString": "enemy_health.expert", - "friendlyName": "Armor-Plated", - "description": "Enemies will be very hard to defeat.", - "defaultValue": 0 - } - } - }, - "pot_shuffle": { - "keyString": "pot_shuffle", - "friendlyName": "Pot Shuffle", - "description": "Keys, items, and buttons hidden under pots in dungeons may be shuffled with other pots in their super-tile.", - "inputType": "range", - "subOptions": { - "on": { - "keyString": "pot_shuffle.on", - "friendlyName": "On", - "description": "Enable pot shuffle.", - "defaultValue": 0 - }, - "off": { - "keyString": "pot_shuffle.off", - "friendlyName": "Off", - "description": "Disable pot shuffle.", - "defaultValue": 50 - } - } - }, - "beemizer_total_chance": { - "keyString": "beemizer_total_chance", - "friendlyName": "Beemizer - Total Chance", - "description": "Chance to replace junk-fill items in the global item pool with single bees and bee traps.", - "inputType": "range", - "subOptions": { - "0": { - "keyString": "beemizer_total_chance.0", - "friendlyName": "Level 0", - "description": "No bee traps are placed.", - "defaultValue": 50 - }, - "25": { - "keyString": "beemizer_total_chance.25", - "friendlyName": "Level 1", - "description": "25% chance for each junk-fill item (rupees, bombs and arrows) to be replaced with bees.", - "defaultValue": 0 - }, - "50": { - "keyString": "beemizer_total_chance.50", - "friendlyName": "Level 2", - "description": "50% chance for each junk-fill item (rupees, bombs and arrows) to be replaced with bees.", - "defaultValue": 0 - }, - "75": { - "keyString": "beemizer_total_chance.75", - "friendlyName": "Level 3", - "description": "75% chance for each junk-fill item (rupees, bombs and arrows) to be replaced with bees.", - "defaultValue": 0 - }, - "100": { - "keyString": "beemizer_total_chance.100", - "friendlyName": "Level 4", - "description": "All junk-fill items (rupees, bombs and arrows) are replaced with bees.", - "defaultValue": 0 - } - } - }, - "beemizer_trap_chance": { - "keyString": "beemizer_trap_chance", - "friendlyName": "Beemizer - Trap Chance", - "description": "Chance that replaced junk-fill items are bee traps.", - "inputType": "range", - "subOptions": { - "60": { - "keyString": "beemizer_trap_chance.60", - "friendlyName": "Level 0", - "description": "60% chance for each beemizer replacement to be a trap (40% chance of a single bee).", - "defaultValue": 50 - }, - "70": { - "keyString": "beemizer_trap_chance.70", - "friendlyName": "Level 1", - "description": "70% chance for each beemizer replacement to be a trap (30% chance of a single bee).", - "defaultValue": 0 - }, - "80": { - "keyString": "beemizer_trap_chance.80", - "friendlyName": "Level 2", - "description": "80% chance for each beemizer replacement to be a trap (20% chance of a single bee).", - "defaultValue": 0 - }, - "90": { - "keyString": "beemizer_trap_chance.90", - "friendlyName": "Level 3", - "description": "90% chance for each beemizer replacement to be a trap (10% chance of a single bee).", - "defaultValue": 0 - }, - "100": { - "keyString": "beemizer_trap_chance.100", - "friendlyName": "Level 4", - "description": "All beemizer replacements are traps (no single bees).", - "defaultValue": 0 - } - } - }, - "shop_shuffle": { - "keyString": "shop_shuffle", - "friendlyName": "Shop Shuffle", - "description": "Alters the inventory and prices of shops.", - "inputType": "range", - "subOptions": { - "none": { - "keyString": "shop_shuffle.none", - "friendlyName": "Vanilla Shops", - "description": "Shop contents are left unchanged.", - "defaultValue": 50 - }, - "g": { - "keyString": "shop_shuffle.g", - "friendlyName": "Pool Shuffle", - "description": "Shuffles the inventory of shops.", - "defaultValue": 0 - }, - "f": { - "keyString": "shop_shuffle.f", - "friendlyName": "Random Shuffle", - "description": "Randomly generate an inventory for each shop from a pool of non-progression items.", - "defaultValue": 0 - }, - "p": { - "keyString": "shop_shuffle.p", - "friendlyName": "Price Shuffle", - "description": "Randomizes the price of items sold in shops.", - "defaultValue": 0 - }, - "u": { - "keyString": "shop_shuffle.u", - "friendlyName": "Capacity Upgrades", - "description": "Shuffles capacity upgrades throughout the game world.", - "defaultValue": 0 - }, - "gp": { - "keyString": "shop_shuffle.gp", - "friendlyName": "Pool & Prices", - "description": "Shuffles the inventory and randomizes the prices of items in shops.", - "defaultValue": 0 - }, - "fp": { - "keyString": "shop_shuffle.fp", - "friendlyName": "Full Shuffle", - "description": "Randomizes the inventory and prices of shops.", - "defaultValue": 0 - }, - "ufp": { - "keyString": "shop_shuffle.ufp", - "friendlyName": "Full Shuffle & Capacity", - "description": "Randomizes the inventory and prices in shops, and distributes capacity upgrades throughout the world.", - "defaultValue": 0 - }, - "wfp": { - "keyString": "shop_shuffle.wfp", - "friendlyName": "Full Shuffle & Potion Shop", - "description": "Randomizes the inventory prices of shops, and shuffles items in the potion shop.", - "defaultValue": 0 - }, - "ufpw": { - "keyString": "shop_shuffle.ufpw", - "friendlyName": "Randomize Everything", - "description": "Randomizes the inventory and prices in shops, distributes capacity upgrades throughout the world, and shuffles items in the potion shop.", - "defaultValue": 0 - } - } - }, - "shop_shuffle_slots": { - "keyString": "shop_shuffle_slots", - "friendlyName": "Pay (Rupees) to Win", - "description": "Move items from the general item pool into shops for purchase.", - "inputType": "range", - "subOptions": { - "0": { - "keyString": "shop_shuffle_slots.0", - "friendlyName": "Off", - "description": "No items are moved", - "defaultValue": 50 - }, - "10": { - "keyString": "shop_shuffle_slots.10", - "friendlyName": "Level 1", - "description": "10 Items are moved into shops.", - "defaultValue": 0 - }, - "20": { - "keyString": "shop_shuffle_slots.20", - "friendlyName": "Level 2", - "description": "20 Items are moved into shops.", - "defaultValue": 0 - }, - "30": { - "keyString": "shop_shuffle_slots.30", - "friendlyName": "Level 3", - "description": "30 Items are moved into shops.", - "defaultValue": 0 - } - } - }, - "shuffle_prizes": { - "keyString": "shuffle_prizes", - "friendlyName": "Prize Shuffle", - "description": "Alters the Prizes from pulling, bonking, enemy kills, digging, and hoarders", - "inputType": "range", - "subOptions": { - "none": { - "keyString": "shuffle_prizes.none", - "friendlyName": "None", - "description": "All prizes from pulling, bonking, enemy kills, digging, hoarders are vanilla.", - "defaultValue": 0 - }, - "g": { - "keyString": "shuffle_prizes.g", - "friendlyName": "\"General\" prize shuffle", - "description": "Shuffles the prizes from pulling, enemy kills, digging, hoarders", - "defaultValue": 50 - }, - "b": { - "keyString": "shuffle_prizes.b", - "friendlyName": "Bonk prize shuffle", - "description": "Shuffles the prizes from bonking into trees.", - "defaultValue": 0 - }, - "bg": { - "keyString": "shuffle_prizes.bg", - "friendlyName": "Both", - "description": "Shuffles both of the options.", - "defaultValue": 0 - } - } - }, - "timer": { - "keyString": "timer", - "friendlyName": "Timed Modes", - "description": "Add a timer to the game UI, and cause it to have various effects.", - "inputType": "range", - "subOptions": { - "none": { - "keyString": "timer.none", - "friendlyName": "Disabled", - "description": "No timed mode is applied to the game.", - "defaultValue": 50 - }, - "timed": { - "keyString": "timer.timed", - "friendlyName": "Timed Mode", - "description": "Starts with clock at zero. Green clocks subtract 4 minutes (total 20). Blue clocks subtract 2 minutes (total 10). Red clocks add two minutes (total 10). Winner is the player with the lowest time at the end.", - "defaultValue": 0 - }, - "timed_ohko": { - "keyString": "timer.timed_ohko", - "friendlyName": "Timed OHKO", - "description": "Starts the clock at ten minutes. Green clocks add five minutes (total 25). As long as the clock as at zero, Link will die in one hit.", - "defaultValue": 0 - }, - "ohko": { - "keyString": "timer.ohko", - "friendlyName": "One-Hit KO", - "description": "Timer always at zero. Permanent OHKO.", - "defaultValue": 0 - }, - "timed_countdown": { - "keyString": "timer.timed_countdown", - "friendlyName": "Timed Countdown", - "description": "Starts the clock with forty minutes. Same clocks as timed mode, but if the clock hits zero you lose. You can still keep playing, though.", - "defaultValue": 0 - }, - "display": { - "keyString": "timer.display", - "friendlyName": "Timer Only", - "description": "Displays a timer, but otherwise does not affect gameplay or the item pool.", - "defaultValue": 0 - } - } - }, - "countdown_start_time": { - "keyString": "countdown_start_time", - "friendlyName": "Countdown Starting Time", - "description": "The amount of time, in minutes, to start with in Timed Countdown and Timed OHKO modes.", - "inputType": "range", - "subOptions": { - "0": { - "keyString": "countdown_start_time.0", - "friendlyName": 0, - "description": "Start with no time on the timer. In Timed OHKO mode, start in OHKO mode.", - "defaultValue": 0 - }, - "10": { - "keyString": "countdown_start_time.10", - "friendlyName": 10, - "description": "Start with 10 minutes on the timer.", - "defaultValue": 50 - }, - "20": { - "keyString": "countdown_start_time.20", - "friendlyName": 20, - "description": "Start with 20 minutes on the timer.", - "defaultValue": 0 - }, - "30": { - "keyString": "countdown_start_time.30", - "friendlyName": 30, - "description": "Start with 30 minutes on the timer.", - "defaultValue": 0 - }, - "60": { - "keyString": "countdown_start_time.60", - "friendlyName": 60, - "description": "Start with an hour on the timer.", - "defaultValue": 0 - } - } - }, - "red_clock_time": { - "keyString": "red_clock_time", - "friendlyName": "Red Clock Time", - "description": "The amount of time, in minutes, to add to or subtract from the timer upon picking up a red clock.", - "inputType": "range", - "subOptions": { - "-2": { - "keyString": "red_clock_time.-2", - "friendlyName": -2, - "description": "Subtract 2 minutes from the timer upon picking up a red clock.", - "defaultValue": 0 - }, - "1": { - "keyString": "red_clock_time.1", - "friendlyName": 1, - "description": "Add a minute to the timer upon picking up a red clock.", - "defaultValue": 50 - } - } - }, - "blue_clock_time": { - "keyString": "blue_clock_time", - "friendlyName": "Blue Clock Time", - "description": "The amount of time, in minutes, to add to or subtract from the timer upon picking up a blue clock.", - "inputType": "range", - "subOptions": { - "1": { - "keyString": "blue_clock_time.1", - "friendlyName": 1, - "description": "Add a minute to the timer upon picking up a blue clock.", - "defaultValue": 0 - }, - "2": { - "keyString": "blue_clock_time.2", - "friendlyName": 2, - "description": "Add 2 minutes to the timer upon picking up a blue clock.", - "defaultValue": 50 - } - } - }, - "green_clock_time": { - "keyString": "green_clock_time", - "friendlyName": "Green Clock Time", - "description": "The amount of time, in minutes, to add to or subtract from the timer upon picking up a green clock.", - "inputType": "range", - "subOptions": { - "4": { - "keyString": "green_clock_time.4", - "friendlyName": 4, - "description": "Add 4 minutes to the timer upon picking up a green clock.", - "defaultValue": 50 - }, - "10": { - "keyString": "green_clock_time.10", - "friendlyName": 10, - "description": "Add 10 minutes to the timer upon picking up a green clock.", - "defaultValue": 0 - }, - "15": { - "keyString": "green_clock_time.15", - "friendlyName": 15, - "description": "Add 15 minutes to the timer upon picking up a green clock.", - "defaultValue": 0 - } - } - }, - "glitch_boots": { - "keyString": "glitch_boots", - "friendlyName": "Glitch Boots", - "description": "Start with Pegasus Boots in any glitched logic mode that makes use of them.", - "inputType": "range", - "subOptions": { - "on": { - "keyString": "glitch_boots.on", - "friendlyName": "On", - "description": "Enable glitch boots.", - "defaultValue": 50 - }, - "off": { - "keyString": "glitch_boots.off", - "friendlyName": "Off", - "description": "Disable glitch boots.", - "defaultValue": 0 - } - } - }, - "door_shuffle": { - "keyString": "door_shuffle", - "friendlyName": "Door Shuffle", - "description": "Shuffles the interior layout of dungeons. Only available if the host rolls the game using the doors version of the generator.", - "inputType": "range", - "subOptions": { - "vanilla": { - "keyString": "door_shuffle.vanilla", - "friendlyName": "Vanilla", - "description": "Doors within dungeons remain unchanged from the vanilla game.", - "defaultValue": 50 - }, - "basic": { - "keyString": "door_shuffle.basic", - "friendlyName": "Basic", - "description": "Dungeons are shuffled within themselves.", - "defaultValue": 0 - }, - "crossed": { - "keyString": "door_shuffle.crossed", - "friendlyName": "Crossed", - "description": "Dungeons are shuffled across each other. Eastern may contain POD, Mire, and Hera.", - "defaultValue": 0 - } - } - }, - "intensity": { - "keyString": "intensity", - "friendlyName": "Door Shuffle Intensity Level", - "description": "Specifies what types of doors will be shuffled.", - "inputType": "range", - "subOptions": { - "1": { - "keyString": "intensity.1", - "friendlyName": "Level 1", - "description": "Doors and spiral staircases will be shuffled amongst themselves.", - "defaultValue": 50 - }, - "2": { - "keyString": "intensity.2", - "friendlyName": "Level 2", - "description": "Doors, open edges, and straight stair cases are shuffled amongst each other. Spiral staircases will be shuffled amongst themselves.", - "defaultValue": 0 - }, - "3": { - "keyString": "intensity.3", - "friendlyName": "Level 3", - "description": "Level 2 plus lobby shuffling, which means any non-dead-end supertile with a south-facing door may become a dungeon entrance.", - "defaultValue": 0 - }, - "random": { - "keyString": "intensity.random", - "friendlyName": "Random", - "description": "Randomly chooses an intensity level from 1-3.", - "defaultValue": 0 - } - } - }, - "key_drop_shuffle": { - "keyString": "key_drop_shuffle", - "friendlyName": "Key Drop Shuffle", - "description": "Allows the small/big keys dropped by enemies/pots to be shuffled into the item pool. This extends the number of checks from 216 to 249", - "inputType": "range", - "subOptions": { - "on": { - "keyString": "key_drop_shuffle.on", - "friendlyName": "Enabled", - "description": "Enables key drop shuffle", - "defaultValue": 0 - }, - "off": { - "keyString": "key_drop_shuffle.off", - "friendlyName": "Disabled", - "description": "Disables key drop shuffle", - "defaultValue": 50 - } - } - } - }, - "romOptions": { - "disablemusic": { - "keyString": "rom.disablemusic", - "friendlyName": "Game Music", - "description": "Enable or disable all in-game music. Sound-effects are unaffected.", - "inputType": "range", - "subOptions": { - "on": { - "keyString": "rom.disablemusic.on", - "friendlyName": "Disabled", - "description": "Disables in-game music.", - "defaultValue": 0 - }, - "off": { - "keyString": "rom.disablemusic.off", - "friendlyName": "Enabled", - "description": "Enables in-game music.", - "defaultValue": 50 - } - } - }, - "reduceflashing": { - "keyString": "rom.reduceflashing", - "friendlyName": "Full-Screen Flashing Effects", - "description": "Enable or disable full-screen flashing effects in game.", - "inputType": "range", - "subOptions": { - "on": { - "keyString": "rom.reduceflashing.on", - "friendlyName": "Disabled", - "description": "Disables flashing.", - "defaultValue": 50 - }, - "off": { - "keyString": "rom.reduceflashing.off", - "friendlyName": "Enabled", - "description": "Enables flashing.", - "defaultValue": 0 - } - } - }, - "quickswap": { - "keyString": "rom.quickswap", - "friendlyName": "Item Quick-Swap", - "description": "Quickly change items by pressing the L+R shoulder buttons. Pressing L+R at the same time toggles the in-slot item (arrows and silver arrows, for example).", - "inputType": "range", - "subOptions": { - "on": { - "keyString": "rom.quickswap.on", - "friendlyName": "Enabled", - "description": "Enable quick-swap.", - "defaultValue": 0 - }, - "off": { - "keyString": "rom.quickswap.off", - "friendlyName": "Disabled", - "description": "Disable quick-swap.", - "defaultValue": 50 - } - } - }, - "triforcehud": { - "keyString": "rom.triforcehud", - "friendlyName": "Triforce Hunt HUD Options", - "description": "Hide the triforce hud in certain circumstances.", - "inputType": "range", - "subOptions": { - "normal": { - "keyString": "rom.triforcehud.normal", - "friendlyName": "Always Show", - "description": "Always display HUD", - "defaultValue": 50 - }, - "hide_goal": { - "keyString": "rom.triforcehud.hide_goal", - "friendlyName": "Hide HUD", - "description": "Hide Triforce HUD elements until a single triforce piece is acquired or you speak to Murahadala", - "defaultValue": 0 - }, - "hide_total": { - "keyString": "rom.triforcehud.hide_required", - "friendlyName": "Hide Total", - "description": "Hide total triforce pieces needed to win the game until you speak with Murahadala", - "defaultValue": 0 - }, - "hide_both": { - "keyString": "rom.triforcehud.hide_both", - "friendlyName": "Hide HUD Total", - "description": "Combination of Hide HUD and Hide Total", - "defaultValue": 0 - } - } - }, - "menuspeed": { - "keyString": "menuspeed", - "friendlyName": "Menu Speed", - "description": "Choose how fast the in-game menu opens and closes.", - "inputType": "range", - "subOptions": { - "normal": { - "keyString": "rom.menuspeed.normal", - "friendlyName": "Vanilla", - "description": "Menu speed is unchanged from the vanilla game.", - "defaultValue": 50 - }, - "instant": { - "keyString": "rom.menuspeed.instant", - "friendlyName": "Instant", - "description": "The in-game menu appears and disappears instantly.", - "defaultValue": 0 - }, - "double": { - "keyString": "rom.menuspeed.double", - "friendlyName": "Double Speed", - "description": "The in-game menu animation moves at double speed.", - "defaultValue": 0 - }, - "triple": { - "keyString": "rom.menuspeed.triple", - "friendlyName": "Triple Speed", - "description": "The in-game menu animation moves at triple speed.", - "defaultValue": 0 - }, - "quadruple": { - "keyString": "rom.menuspeed.quadruple", - "friendlyName": "Quadruple Speed", - "description": "The in-game menu animation moves at quadruple speed.", - "defaultValue": 0 - }, - "half": { - "keyString": "rom.menuspeed.half", - "friendlyName": "Half Speed", - "description": "The in-game menu animation moves at half speed.", - "defaultValue": 0 - } - } - }, - "heartcolor": { - "keyString": "rom.heartcolor", - "friendlyName": "Heart Color", - "description": "Changes the color of your in-game health hearts.", - "inputType": "range", - "subOptions": { - "red": { - "keyString": "rom.heartcolor.red", - "friendlyName": "Red", - "description": "Health hearts are red.", - "defaultValue": 50 - }, - "blue": { - "keyString": "rom.heartcolor.blue", - "friendlyName": "Blue", - "description": "Health hearts are blue.", - "defaultValue": 0 - }, - "green": { - "keyString": "rom.heartcolor.green", - "friendlyName": "Green", - "description": "Health hearts are green.", - "defaultValue": 0 - }, - "yellow": { - "keyString": "rom.heartcolor.yellow", - "friendlyName": "Yellow", - "description": "Health hearts are yellow.", - "defaultValue": 0 - }, - "random": { - "keyString": "rom.heartcolor.random", - "friendlyName": "Random", - "description": "Health heart color is chosen randomly from red, green, blue, and yellow.", - "defaultValue": 0 - } - } - }, - "heartbeep": { - "keyString": "rom.heartbeep", - "friendlyName": "Heart Beep Speed", - "description": "Controls the frequency of the low-health beeping.", - "inputType": "range", - "subOptions": { - "double": { - "keyString": "rom.heartbeep.double", - "friendlyName": "Double", - "description": "Doubles the frequency of the low-health beep.", - "defaultValue": 0 - }, - "normal": { - "keyString": "rom.heartbeep.normal", - "friendlyName": "Vanilla", - "description": "Heart beep frequency is unchanged from the vanilla game.", - "defaultValue": 50 - }, - "half": { - "keyString": "rom.heartbeep.half", - "friendlyName": "Half Speed", - "description": "Heart beep plays at half-speed.", - "defaultValue": 0 - }, - "quarter": { - "keyString": "rom.heartbeep.quarter", - "friendlyName": "Quarter Speed", - "description": "Heart beep plays at one quarter-speed.", - "defaultValue": 0 - }, - "off": { - "keyString": "rom.heartbeep.off", - "friendlyName": "Disabled", - "description": "Disables the low-health heart beep.", - "defaultValue": 0 - } - } - }, - "ow_palettes": { - "keyString": "rom.ow_palettes", - "friendlyName": "Overworld Palette", - "description": "Randomize the colors of the overworld, within reason.", - "inputType": "range", - "subOptions": { - "default": { - "keyString": "rom.ow_palettes.default", - "friendlyName": "Vanilla", - "description": "Overworld colors will remain unchanged.", - "defaultValue": 50 - }, - "random": { - "keyString": "rom.ow_palettes.random", - "friendlyName": "Random", - "description": "Shuffles the colors of the overworld palette.", - "defaultValue": 0 - }, - "blackout": { - "keyString": "rom.ow_palettes.blackout", - "friendlyName": "Blackout", - "description": "Never use this. Makes all overworld palette colors black.", - "defaultValue": 0 - }, - "grayscale": { - "keyString": "rom.ow_palettes.grayscale", - "friendlyName": "Grayscale", - "description": "Removes all saturation of colors.", - "defaultValue": 0 - }, - "negative": { - "keyString": "rom.ow_palettes.negative", - "friendlyName": "Negative", - "description": "Invert all colors", - "defaultValue": 0 - }, - "classic": { - "keyString": "rom.ow_palettes.classic", - "friendlyName": "Classic", - "description": "Produces results similar to the website.", - "defaultValue": 0 - }, - "dizzy": { - "keyString": "rom.ow_palettes.dizzy", - "friendlyName": "Dizzy", - "description": "No logic in colors but saturation and lightness are conserved.", - "defaultValue": 0 - }, - "sick": { - "keyString": "rom.ow_palettes.sick", - "friendlyName": "Sick", - "description": "No logic in colors but lightness is conserved.", - "defaultValue": 0 - }, - "puke": { - "keyString": "rom.ow_palettes.puke", - "friendlyName": "Puke", - "description": "No logic at all.", - "defaultValue": 0 - } - } - }, - "uw_palettes": { - "keyString": "rom.uw_palettes", - "friendlyName": "Underworld Palettes", - "description": "Randomize the colors of the underworld (caves, dungeons, etc.), within reason.", - "inputType": "range", - "subOptions": { - "default": { - "keyString": "rom.uw_palettes.default", - "friendlyName": "Vanilla", - "description": "Underworld colors will remain unchanged.", - "defaultValue": 50 - }, - "random": { - "keyString": "rom.uw_palettes.random", - "friendlyName": "Random", - "description": "Shuffles the colors of the underworld palette.", - "defaultValue": 0 - }, - "blackout": { - "keyString": "rom.uw_palettes.blackout", - "friendlyName": "Blackout", - "description": "Never use this. Makes all underworld palette colors black.", - "defaultValue": 0 - }, - "grayscale": { - "keyString": "rom.uw_palettes.grayscale", - "friendlyName": "Grayscale", - "description": "Removes all saturation of colors.", - "defaultValue": 0 - }, - "negative": { - "keyString": "rom.uw_palettes.negative", - "friendlyName": "Negative", - "description": "Invert all colors", - "defaultValue": 0 - }, - "classic": { - "keyString": "rom.uw_palettes.classic", - "friendlyName": "Classic", - "description": "Produces results similar to the website.", - "defaultValue": 0 - }, - "dizzy": { - "keyString": "rom.uw_palettes.dizzy", - "friendlyName": "Dizzy", - "description": "No logic in colors but saturation and lightness are conserved.", - "defaultValue": 0 - }, - "sick": { - "keyString": "rom.uw_palettes.sick", - "friendlyName": "Sick", - "description": "No logic in colors but lightness is conserved.", - "defaultValue": 0 - }, - "puke": { - "keyString": "rom.uw_palettes.puke", - "friendlyName": "Puke", - "description": "No logic at all.", - "defaultValue": 0 - } - } - }, - "hud_palettes": { - "keyString": "rom.hud_palettes", - "friendlyName": "HUD Palettes", - "description": "Randomize the colors of the HUD (user interface), within reason.", - "inputType": "range", - "subOptions": { - "default": { - "keyString": "rom.hud_palettes.default", - "friendlyName": "Vanilla", - "description": "HUD colors will remain unchanged.", - "defaultValue": 50 - }, - "random": { - "keyString": "rom.hud_palettes.random", - "friendlyName": "Random", - "description": "Shuffles the colors of the HUD palette.", - "defaultValue": 0 - }, - "blackout": { - "keyString": "rom.hud_palettes.blackout", - "friendlyName": "Blackout", - "description": "Never use this. Makes all HUD palette colors black.", - "defaultValue": 0 - }, - "grayscale": { - "keyString": "rom.hud_palettes.grayscale", - "friendlyName": "Grayscale", - "description": "Removes all saturation of colors.", - "defaultValue": 0 - }, - "negative": { - "keyString": "rom.hud_palettes.negative", - "friendlyName": "Negative", - "description": "Invert all colors", - "defaultValue": 0 - }, - "classic": { - "keyString": "rom.hud_palettes.classic", - "friendlyName": "Classic", - "description": "Produces results similar to the website.", - "defaultValue": 0 - }, - "dizzy": { - "keyString": "rom.hud_palettes.dizzy", - "friendlyName": "Dizzy", - "description": "No logic in colors but saturation and lightness are conserved.", - "defaultValue": 0 - }, - "sick": { - "keyString": "rom.hud_palettes.sick", - "friendlyName": "Sick", - "description": "No logic in colors but lightness is conserved.", - "defaultValue": 0 - }, - "puke": { - "keyString": "rom.hud_palettes.puke", - "friendlyName": "Puke", - "description": "No logic at all.", - "defaultValue": 0 - } - } - }, - "shield_palettes": { - "keyString": "rom.shield_palettes", - "friendlyName": "Shield Palettes", - "description": "Randomize the colors of the shield, within reason.", - "inputType": "range", - "subOptions": { - "default": { - "keyString": "rom.shield_palettes.default", - "friendlyName": "Vanilla", - "description": "Shield colors will remain unchanged.", - "defaultValue": 50 - }, - "random": { - "keyString": "rom.shield_palettes.random", - "friendlyName": "Random", - "description": "Shuffles the colors of the shield palette.", - "defaultValue": 0 - }, - "blackout": { - "keyString": "rom.shield_palettes.blackout", - "friendlyName": "Blackout", - "description": "Never use this. Makes all shield palette colors black.", - "defaultValue": 0 - }, - "grayscale": { - "keyString": "rom.shield_palettes.grayscale", - "friendlyName": "Grayscale", - "description": "Removes all saturation of colors.", - "defaultValue": 0 - }, - "negative": { - "keyString": "rom.shield_palettes.negative", - "friendlyName": "Negative", - "description": "Invert all colors", - "defaultValue": 0 - }, - "classic": { - "keyString": "rom.shield_palettes.classic", - "friendlyName": "Classic", - "description": "Produces results similar to the website.", - "defaultValue": 0 - }, - "dizzy": { - "keyString": "rom.shield_palettes.dizzy", - "friendlyName": "Dizzy", - "description": "No logic in colors but saturation and lightness are conserved.", - "defaultValue": 0 - }, - "sick": { - "keyString": "rom.shield_palettes.sick", - "friendlyName": "Sick", - "description": "No logic in colors but lightness is conserved.", - "defaultValue": 0 - }, - "puke": { - "keyString": "rom.shield_palettes.puke", - "friendlyName": "Puke", - "description": "No logic at all.", - "defaultValue": 0 - } - } - }, - "sword_palettes": { - "keyString": "rom.sword_palettes", - "friendlyName": "Sword Palettes", - "description": "Randomize the colors of the sword, within reason.", - "inputType": "range", - "subOptions": { - "default": { - "keyString": "rom.sword_palettes.default", - "friendlyName": "Vanilla", - "description": "Sword colors will remain unchanged.", - "defaultValue": 50 - }, - "random": { - "keyString": "rom.sword_palettes.random", - "friendlyName": "Random", - "description": "Shuffles the colors of the sword palette.", - "defaultValue": 0 - }, - "blackout": { - "keyString": "rom.sword_palettes.blackout", - "friendlyName": "Blackout", - "description": "Never use this. Makes all sword palette colors black.", - "defaultValue": 0 - }, - "grayscale": { - "keyString": "rom.sword_palettes.grayscale", - "friendlyName": "Grayscale", - "description": "Removes all saturation of colors.", - "defaultValue": 0 - }, - "negative": { - "keyString": "rom.sword_palettes.negative", - "friendlyName": "Negative", - "description": "Invert all colors", - "defaultValue": 0 - }, - "classic": { - "keyString": "rom.sword_palettes.classic", - "friendlyName": "Classic", - "description": "Produces results similar to the website.", - "defaultValue": 0 - }, - "dizzy": { - "keyString": "rom.sword_palettes.dizzy", - "friendlyName": "Dizzy", - "description": "No logic in colors but saturation and lightness are conserved.", - "defaultValue": 0 - }, - "sick": { - "keyString": "rom.sword_palettes.sick", - "friendlyName": "Sick", - "description": "No logic in colors but lightness is conserved.", - "defaultValue": 0 - }, - "puke": { - "keyString": "rom.sword_palettes.puke", - "friendlyName": "Puke", - "description": "No logic at all.", - "defaultValue": 0 - } - } - } - } -} \ No newline at end of file diff --git a/WebHostLib/static/static/weightedSettings.yaml b/WebHostLib/static/static/weightedSettings.yaml deleted file mode 100644 index c3667d47..00000000 --- a/WebHostLib/static/static/weightedSettings.yaml +++ /dev/null @@ -1,449 +0,0 @@ -# What is this file? -# This file contains options which allow you to configure your multiworld experience while allowing others -# to play how they want as well. - -# How do I use it? -# The options in this file are weighted. This means the higher number you assign to a value, the more -# chances you have for that option to be chosen. For example, an option like this: -# -# map_shuffle: -# on: 5 -# off: 15 -# -# Means you have 5 chances for map shuffle to occur, and 15 chances for map shuffle to be turned off - -# I've never seen a file like this before. What characters am I allowed to use? -# This is a .yaml file. You are allowed to use most characters. -# To test if your yaml is valid or not, you can use this website: -# http://www.yamllint.com/ - -# For use with the weighted-settings page on the website. Changing this value will cause all users to be prompted -# to update their settings. The version number should match the current released version number, and the revision -# should be updated manually by whoever edits this file. -ws_version: 4.1.1 rev0 - -description: Template Name # Used to describe your yaml. Useful if you have multiple files -name: YourName # Your name in-game. Spaces will be replaced with underscores and there is a 16 character limit -### Logic Section ### -glitches_required: # Determine the logic required to complete the seed - none: 50 # No glitches required - minor_glitches: 0 # Puts fake flipper, waterwalk, super bunny shenanigans, and etc into logic - overworld_glitches: 0 # Assumes the player has knowledge of both overworld major glitches (boots clips, mirror clips) and minor glitches (fake flipper, super bunny shenanigans, water walk and etc.) - no_logic: 0 # Your own items are placed with no regard to any logic; such as your Fire Rod can be on your Trinexx. - # Other players items are placed into your world under OWG logic -dark_room_logic: # Logic for unlit dark rooms - lamp: 50 # require the Lamp for these rooms to be considered accessible. - torches: 0 # in addition to lamp, allow the fire rod and presence of easily accessible torches for access - none: 0 # all dark rooms are always considered doable, meaning this may force completion of rooms in complete darkness -restrict_dungeon_item_on_boss: # aka ambrosia boss items - on: 0 # prevents unshuffled compasses, maps and keys to be boss drops, they can still drop keysanity and other players' items - off: 50 -### End of Logic Section ### -meta_ignore: # Nullify options specified in the meta.yaml file. Adding an option here guarantees it will not occur in your seed, even if the .yaml file specifies it - mode: - - inverted # Never play inverted seeds - retro: - - on # Never play retro seeds - weapons: - - swordless # Never play a swordless seed -map_shuffle: # Shuffle dungeon maps into the world and other dungeons, including other players' worlds - on: 0 - off: 50 -compass_shuffle: # Shuffle compasses into the world and other dungeons, including other players' worlds - on: 0 - off: 50 -smallkey_shuffle: # Shuffle small keys into the world and other dungeons, including other players' worlds - on: 0 - universal: 0 # allows small keys to be used in any dungeon and adds shops to buy more - off: 50 -bigkey_shuffle: # Shuffle big keys into the world and other dungeons, including other players' worlds - on: 0 - off: 50 -local_keys: # Keep small keys and big keys local to your world - on: 0 - off: 50 -dungeon_items: # Alternative to the 4 shuffles and local_keys above this, does nothing until the respective 4 shuffles and local_keys above are deleted - mc: 0 # Shuffle maps and compasses - none: 50 # Shuffle none of the 4 - mcsb: 0 # Shuffle all of the 4, any combination of m, c, s and b will shuffle the respective item, or not if it's missing, so you can add more options here - lmcsb: 0 # Like mcsb above, but with keys kept local to your world. l is what makes your keys local, or not if it's missing - ub: 0 # universal small keys and shuffled big keys - # you can add more combos of these letters here -dungeon_counters: - on: 0 # Always display amount of items checked in a dungeon - pickup: 50 # Show when compass is picked up - default: 0 # Show when compass is picked up if the compass itself is shuffled - off: 0 # Never show item count in dungeons -accessibility: - items: 0 # Guarantees you will be able to acquire all items, but you may not be able to access all locations - locations: 50 # Guarantees you will be able to access all locations, and therefore all items - none: 0 # Guarantees only that the game is beatable. You may not be able to access all locations or acquire all items -progressive: # Enable or disable progressive items (swords, shields, bow) - on: 50 # All items are progressive - off: 0 # No items are progressive - random: 0 # Randomly decides for all items. Swords could be progressive, shields might not be -entrance_shuffle: # Documentation: https://alttpr.com/en/options#entrance_shuffle - none: 50 # Vanilla game map. All entrances and exits lead to their original locations. You probably want this option - dungeonssimple: 0 # Shuffle just dungeons amongst each other, swapping dungeons entirely, so Hyrule Castle is always 1 dungeon - dungeonsfull: 0 # Shuffle any dungeon entrance with any dungeon interior, so Hyrule Castle can be 4 different dungeons - simple: 0 # Entrances are grouped together before being randomized. Simple uses the most strict grouping rules - restricted: 0 # Less strict than simple - full: 0 # Less strict than restricted - crossed: 0 # Less strict than full - insanity: 0 # Very few grouping rules. Good luck -goals: - ganon: 50 # Climb GT, defeat Agahnim 2, and then kill Ganon - fast_ganon: 0 # Only killing Ganon is required. The hole is always open. However, items may still be placed in GT - dungeons: 0 # Defeat the boss of all dungeons, including Agahnim's tower and GT (Aga 2) - pedestal: 0 # Pull the Triforce from the Master Sword pedestal - ganon_pedestal: 0 # Pull the Master Sword pedestal, then kill Ganon - triforce_hunt: 0 # Collect 20 of 30 Triforce pieces spread throughout the worlds, then turn them in to Murahadala in front of Hyrule Castle - local_triforce_hunt: 0 # Collect 20 of 30 Triforce pieces spread throughout your world, then turn them in to Murahadala in front of Hyrule Castle - ganon_triforce_hunt: 0 # Collect 20 of 30 Triforce pieces spread throughout the worlds, then kill Ganon - local_ganon_triforce_hunt: 0 # Collect 20 of 30 Triforce pieces spread throughout your world, then kill Ganon - ice_rod_hunt: 0 # You start with everything needed to 216 the seed. Find the Ice rod, then kill Trinexx at Turtle rock. -pyramid_open: - goal: 50 # Opens the pyramid if the goal requires you to kill Ganon, unless the goal is Slow Ganon or All Dungeons - auto: 0 # Same as Goal, but also opens when any non-dungeon entrance shuffle is used - yes: 0 # Pyramid hole is always open. Ganon's vulnerable condition is still required before he can he hurt - no: 0 # Pyramid hole is always closed until you defeat Agahnim atop Ganon's Tower -triforce_pieces_mode: #Determine how to calculate the extra available triforce pieces. - extra: 0 # available = triforce_pieces_extra + triforce_pieces_required - percentage: 0 # available = (triforce_pieces_percentage /100) * triforce_pieces_required - available: 50 # available = triforce_pieces_available -triforce_pieces_extra: # Set to how many extra triforces pieces are available to collect in the world. - # Format "pieces: chance" - 0: 0 - 5: 50 - 10: 50 - 15: 0 - 20: 0 -triforce_pieces_percentage: # Set to how many triforce pieces according to a percentage of the required ones, are available to collect in the world. - # Format "pieces: chance" - 100: 0 #No extra - 150: 50 #Half the required will be added as extra - 200: 0 #There are the double of the required ones available. -triforce_pieces_available: # Set to how many triforces pieces are available to collect in the world. Default is 30. Max is 90, Min is 1 - # Format "pieces: chance" - 25: 0 - 30: 50 - 40: 0 - 50: 0 -triforce_pieces_required: # Set to how many out of X triforce pieces you need to win the game in a triforce hunt. Default is 20. Max is 90, Min is 1 - # Format "pieces: chance" - 15: 0 - 20: 50 - 30: 0 - 40: 0 - 50: 0 -tower_open: # Crystals required to open GT - '0': 80 - '1': 70 - '2': 60 - '3': 50 - '4': 40 - '5': 30 - '6': 20 - '7': 10 - random: 0 -ganon_open: # Crystals required to hurt Ganon - '0': 80 - '1': 70 - '2': 60 - '3': 50 - '4': 40 - '5': 30 - '6': 20 - '7': 10 - random: 0 -mode: - standard: 50 # Begin the game by rescuing Zelda from her cell and escorting her to the Sanctuary - open: 50 # Begin the game from your choice of Link's House or the Sanctuary - inverted: 0 # Begin in the Dark World. The Moon Pearl is required to avoid bunny-state in Light World, and the Light World game map is altered -retro: - on: 0 # you must buy a quiver to use the bow, take-any caves and an old-man cave are added to the world. You may need to find your sword from the old man's cave - off: 50 -hints: - 'on': 50 # Hint tiles sometimes give item location hints - 'off': 0 # Hint tiles provide gameplay tips -weapons: # Specifically, swords - randomized: 0 # Swords are placed randomly throughout the world - assured: 50 # Begin with a sword, the rest are placed randomly throughout the world - vanilla: 0 # Swords are placed in vanilla locations in your own game (Uncle, Pyramid Fairy, Smiths, Pedestal) - swordless: 0 # Your swords are replaced by rupees. Gameplay changes have been made to accommodate this change -item_pool: - easy: 0 # Doubled upgrades, progressives, and etc - normal: 50 # Item availability remains unchanged from vanilla game - hard: 0 # Reduced upgrade availability (max: 14 hearts, blue mail, tempered sword, fire shield, no silvers unless swordless) - expert: 0 # Minimum upgrade availability (max: 8 hearts, green mail, master sword, fighter shield, no silvers unless swordless) -item_functionality: - easy: 0 # Allow Hammer to damage ganon, Allow Hammer tablet collection, Allow swordless medallion use everywhere. - normal: 50 # Vanilla item functionality - hard: 0 # Reduced helpfulness of items (potions less effective, can't catch faeries, cape uses double magic, byrna does not grant invulnerability, boomerangs do not stun, silvers disabled outside ganon) - expert: 0 # Vastly reduces the helpfulness of items (potions barely effective, can't catch faeries, cape uses double magic, byrna does not grant invulnerability, boomerangs and hookshot do not stun, silvers disabled outside ganon) -progression_balancing: - on: 50 # A system to reduce BK, as in times during which you can't do anything by moving your items into an earlier access sphere to make it likely you have stuff to do - off: 0 # Turn this off if you don't mind a longer multiworld, or can glitch around missing items. -tile_shuffle: # Randomize the tile layouts in flying tile rooms - on: 0 - off: 50 -misery_mire_medallion: # required medallion to open Misery Mire front entrance - random: 50 - ether: 0 - bombos: 0 - quake: 0 -turtle_rock_medallion: # required medallion to open Turtle Rock front entrance - random: 50 - ether: 0 - bombos: 0 - quake: 0 -### Enemizer Section ### -boss_shuffle: - none: 50 # Vanilla bosses - simple: 0 # Existing bosses except Ganon and Agahnim are shuffled throughout dungeons - full: 0 # 3 bosses can occur twice - random: 0 # Any boss can appear any amount of times - singularity: 0 # Picks a boss, tries to put it everywhere that works, if there's spaces remaining it picks a boss to fill those -enemy_shuffle: # Randomize enemy placement - on: 0 - off: 50 -killable_thieves: # Make thieves killable - on: 0 # Usually turned on together with enemy_shuffle to make annoying thief placement more manageable - off: 50 -bush_shuffle: # Randomize the chance that bushes have enemies and the enemies under said bush - on: 0 - off: 50 -enemy_damage: - default: 50 # Vanilla enemy damage - shuffled: 0 # Enemies deal 0 to 4 hearts and armor helps - random: 0 # Enemies deal 0 to 8 hearts and armor just reshuffles the damage -enemy_health: - default: 50 # Vanilla enemy HP - easy: 0 # Enemies have reduced health - hard: 0 # Enemies have increased health - expert: 0 # Enemies have greatly increased health -pot_shuffle: - 'on': 0 # Keys, items, and buttons hidden under pots in dungeons are shuffled with other pots in their supertile - 'off': 50 # Default pot item locations -### End of Enemizer Section ### -# can add weights for any whole number between 0 and 100 -beemizer_total_chance: # Remove items from the global item pool and replace them with single bees (fill bottles) and bee traps - 0: 50 # No junk fill items are replaced (Beemizer is off) - 25: 0 # 25% chance for each junk fill item (rupees, bombs and arrows) to be replaced with bees - 50: 0 # 50% chance for each junk fill item (rupees, bombs and arrows) to be replaced with bees - 75: 0 # 75% chance for each junk fill item (rupees, bombs and arrows) to be replaced with bees - 100: 0 # All junk fill items (rupees, bombs and arrows) are replaced with bees -beemizer_trap_chance: - 60: 50 # 60% chance for each beemizer replacement to be a trap, 40% chance to be a single bee - 70: 0 # 70% chance for each beemizer replacement to be a trap, 30% chance to be a single bee - 80: 0 # 80% chance for each beemizer replacement to be a trap, 20% chance to be a single bee - 90: 0 # 90% chance for each beemizer replacement to be a trap, 10% chance to be a single bee - 100: 0 # All beemizer replacements are traps -### Shop Settings ### -shop_shuffle_slots: # Maximum amount of shop slots to be filled with regular item pool items (such as Moon Pearl) - 0: 50 - 10: 0 - 20: 0 - 30: 0 -shop_shuffle: - none: 50 - g: 0 # Generate new default inventories for overworld/underworld shops, and unique shops - f: 0 # Generate new default inventories for every shop independently - p: 0 # Randomize the prices of the items in shop inventories - u: 0 # Shuffle capacity upgrades into the item pool (and allow them to traverse the multiworld) - w: 0 # Consider witch's hut like any other shop and shuffle/randomize it too - gp: 0 # Shuffle inventories and randomize prices - fp: 0 # Randomize items in every shop and their prices - ufp: 0 # Randomize items and prices in every shop, and include capacity upgrades in item pool - wfp: 0 # Randomize items and prices in every shop, and include potion shop inventory in shuffle - ufpw: 0 # Randomize items and prices in every shop, shuffle potion shop inventory, and include capacity upgrades - # You can add more combos -### End of Shop Section ### -shuffle_prizes: # aka drops - none: 0 # do not shuffle prize packs - g: 50 # shuffle "general" price packs, as in enemy, tree pull, dig etc. - b: 0 # shuffle "bonk" price packs - bg: 0 # shuffle both -timer: - none: 50 # No timer will be displayed. - timed: 0 # Starts with clock at zero. Green clocks subtract 4 minutes (total 20). Blue clocks subtract 2 minutes (total 10). Red clocks add two minutes (total 10). Winner is the player with the lowest time at the end. - timed_ohko: 0 # Starts the clock at ten minutes. Green clocks add five minutes (total 25). As long as the clock as at zero, Link will die in one hit. - ohko: 0 # Timer always at zero. Permanent OHKO. - timed_countdown: 0 # Starts the clock with forty minutes. Same clocks as timed mode, but if the clock hits zero you lose. You can still keep playing, though. - display: 0 # Displays a timer, but otherwise does not affect gameplay or the item pool. -countdown_start_time: # For timed_ohko and timed_countdown timer modes, the amount of time in minutes to start with - 0: 0 # For timed_ohko, starts in OHKO mode when starting the game - 10: 50 - 20: 0 - 30: 0 - 60: 0 -red_clock_time: # For all timer modes, the amount of time in minutes to gain or lose when picking up a red clock - -2: 50 - 1: 0 -blue_clock_time: # For all timer modes, the amount of time in minutes to gain or lose when picking up a blue clock - 1: 0 - 2: 50 -green_clock_time: # For all timer modes, the amount of time in minutes to gain or lose when picking up a green clock - 4: 50 - 10: 0 - 15: 0 -# Can be uncommented to use it -# local_items: # Force certain items to appear in your world only, not across the multiworld. Recognizes some group names, like "Swords" -# - "Moon Pearl" -# - "Small Keys" -# - "Big Keys" -# Can be uncommented to use it -# startinventory: # Begin the file with the listed items/upgrades - # Pegasus Boots: on - # Bomb Upgrade (+10): 4 - # Arrow Upgrade (+10): 4 -glitch_boots: - on: 50 # Start with Pegasus Boots in any glitched logic mode that makes use of them - off: 0 -linked_options: - - name: crosskeys - options: # These overwrite earlier options if the percentage chance triggers - entrance_shuffle: crossed - bigkey_shuffle: true - compass_shuffle: true - map_shuffle: true - smallkey_shuffle: true - percentage: 0 # Set this to the percentage chance you want crosskeys - - name: localcrosskeys - options: # These overwrite earlier options if the percentage chance triggers - entrance_shuffle: crossed - bigkey_shuffle: true - compass_shuffle: true - map_shuffle: true - smallkey_shuffle: true - local_items: # Forces keys to be local to your own world - - "Small Keys" - - "Big Keys" - percentage: 0 # Set this to the percentage chance you want local crosskeys - - name: enemizer - options: - boss_shuffle: # Subchances can be injected too, which then get rolled - simple: 1 - full: 1 - random: 1 - singularity: 1 - enemy_damage: - shuffled: 1 - random: 1 - enemy_health: - easy: 1 - hard: 1 - expert: 1 - percentage: 0 # Set this to the percentage chance you want enemizer -### door rando only options ### -door_shuffle: # Only available if the host uses the doors branch, it is ignored otherwise - vanilla: 50 # Everything should be like in vanilla - basic: 0 # Dungeons are shuffled within themselves - crossed: 0 # Dungeons are shuffled across each other -intensity: # Only available if the host uses the doors branch, it is ignored otherwise - 1: 50 # Shuffles normal doors and spiral staircases - 2: 0 # And shuffles open edges and straight staircases - 3: 0 # And shuffles dungeon lobbies - random: 0 # Picks one of those at random -key_drop_shuffle: # Only available if the host uses the doors branch, it is ignored otherwise - on: 0 # Enables the small keys dropped by enemies or under pots, and the big key dropped by the Ball & Chain guard to be shuffled into the pool. This extends the number of checks to 249. - off: 50 -experimental: # Only available if the host uses the doors branch, it is ignored otherwise - on: 0 # Enables experimental features. - off: 50 -debug: # Only available if the host uses the doors branch, it is ignored otherwise - on: 0 # Enables debugging features. Currently, these are the Item collection counter. (overwrites total triforce pieces) and Castle Gate closed indicator. - off: 50 -### end of door rando only options ### -rom: - #sprite_pool: # When specified, limits the pool of sprites used for randomon-event to the specified pool. Uncomment to use this. - # - link - # - pride link - # - penguin link - # - random # You can specify random multiple times for however many potentially unique random sprites you want in your pool. - sprite: # Enter the name of your preferred sprite and weight it appropriately - random: 0 - link: 50 # To add other sprites: open the gui/Creator, go to adjust, select a sprite and write down the name the gui calls it - disablemusic: # If "on", all in-game music will be disabled - on: 0 - off: 50 - quickswap: # Enable switching items by pressing the L+R shoulder buttons - on: 50 - off: 0 - triforcehud: # Disable visibility of the triforce hud unless collecting a piece or speaking to Murahadala - normal: 50 # original behavior (always visible) - hide_goal: 0 # hide counter until a piece is collected or speaking to Murahadala - hide_required: 0 # Always visible, but required amount is invisible until determined by Murahadala - hide_both: 0 # Hide both under above circumstances - reduceflashing: # Reduces instances of flashing such as lightning attacks, weather, ether and more. - on: 50 - off: 0 - menuspeed: # Controls how fast the item menu opens and closes - normal: 50 - instant: 0 - double: 0 - triple: 0 - quadruple: 0 - half: 0 - heartcolor: # Controls the color of your health hearts - red: 50 - blue: 0 - green: 0 - yellow: 0 - random: 0 - heartbeep: # Controls the frequency of the low-health beeping - double: 0 - normal: 50 - half: 0 - quarter: 0 - off: 0 - ow_palettes: # Change the colors of the overworld - default: 50 # No changes - random: 0 # Shuffle the colors, with harmony in mind - blackout: 0 # everything black / blind mode - grayscale: 0 - negative: 0 - classic: 0 - dizzy: 0 - sick: 0 - puke: 0 - uw_palettes: # Change the colors of caves and dungeons - default: 50 # No changes - random: 0 # Shuffle the colors, with harmony in mind - blackout: 0 # everything black / blind mode - grayscale: 0 - negative: 0 - classic: 0 - dizzy: 0 - sick: 0 - puke: 0 - hud_palettes: # Change the colors of the hud - default: 50 # No changes - random: 0 # Shuffle the colors, with harmony in mind - blackout: 0 # everything black / blind mode - grayscale: 0 - negative: 0 - classic: 0 - dizzy: 0 - sick: 0 - puke: 0 - sword_palettes: # Change the colors of swords - default: 50 # No changes - random: 0 # Shuffle the colors, with harmony in mind - blackout: 0 # everything black / blind mode - grayscale: 0 - negative: 0 - classic: 0 - dizzy: 0 - sick: 0 - puke: 0 - shield_palettes: # Change the colors of shields - default: 50 # No changes - random: 0 # Shuffle the colors, with harmony in mind - blackout: 0 # everything black / blind mode - grayscale: 0 - negative: 0 - classic: 0 - dizzy: 0 - sick: 0 - puke: 0 diff --git a/WebHostLib/static/styles/supermetroidTracker.css b/WebHostLib/static/styles/supermetroidTracker.css new file mode 100644 index 00000000..34dbeaf2 --- /dev/null +++ b/WebHostLib/static/styles/supermetroidTracker.css @@ -0,0 +1,104 @@ +#player-tracker-wrapper{ + margin: 0; +} + +#inventory-table{ + border-top: 2px solid #000000; + border-left: 2px solid #000000; + border-right: 2px solid #000000; + border-top-left-radius: 4px; + border-top-right-radius: 4px; + padding: 3px 3px 10px; + width: 384px; + background-color: #546E7A; +} + +#inventory-table td{ + width: 45px; + height: 45px; + text-align: center; + vertical-align: middle; +} + +#inventory-table img{ + height: 100%; + max-width: 40px; + max-height: 40px; + min-width: 40px; + min-height: 40px; + filter: grayscale(100%) contrast(75%) brightness(30%); +} + +#inventory-table img.acquired{ + filter: none; +} + +#inventory-table div.counted-item { + position: relative; +} + +#inventory-table div.item-count { + position: absolute; + color: white; + font-family: "Minecraftia", monospace; + font-weight: bold; + bottom: 0px; + right: 0px; +} + +#location-table{ + width: 384px; + border-left: 2px solid #000000; + border-right: 2px solid #000000; + border-bottom: 2px solid #000000; + border-bottom-left-radius: 4px; + border-bottom-right-radius: 4px; + background-color: #546E7A; + color: #000000; + padding: 0 3px 3px; + font-size: 14px; + cursor: default; +} + +#location-table th{ + vertical-align: middle; + text-align: left; + padding-right: 10px; +} + +#location-table td{ + padding-top: 2px; + padding-bottom: 2px; + line-height: 20px; +} + +#location-table td.counter { + text-align: right; + font-size: 14px; +} + +#location-table td.toggle-arrow { + text-align: right; +} + +#location-table tr#Total-header { + font-weight: bold; +} + +#location-table img{ + height: 100%; + max-width: 30px; + max-height: 30px; +} + +#location-table tbody.locations { + font-size: 12px; +} + +#location-table td.location-name { + padding-left: 16px; +} + +.hide { + display: none; +} diff --git a/WebHostLib/templates/minecraftTracker.html b/WebHostLib/templates/minecraftTracker.html index 5389b025..c1d6fa97 100644 --- a/WebHostLib/templates/minecraftTracker.html +++ b/WebHostLib/templates/minecraftTracker.html @@ -42,6 +42,7 @@ + diff --git a/WebHostLib/templates/supermetroidTracker.html b/WebHostLib/templates/supermetroidTracker.html new file mode 100644 index 00000000..342f7564 --- /dev/null +++ b/WebHostLib/templates/supermetroidTracker.html @@ -0,0 +1,85 @@ + + + + {{ player_name }}'s Tracker + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ +
{{ energy_count }}
+
+
+
+ +
{{ reserve_count }}
+
+
+
+ +
{{ missile_count }}
+
+
+
+ +
{{ super_count }}
+
+
+
+ +
{{ power_count }}
+
+
+ + {% for area in checks_done %} + + + + + + {% for location in location_info[area] %} + + + + + {% endfor %} + + {% endfor %} +
{{ area }} {{'▼' if area != 'Total'}}{{ checks_done[area] }} / {{ checks_in_area[area] }}
{{ location }}{{ '✔' if location_info[area][location] else '' }}
+
+ + diff --git a/WebHostLib/templates/tracker.html b/WebHostLib/templates/tracker.html index 40faddfc..889ed2b2 100644 --- a/WebHostLib/templates/tracker.html +++ b/WebHostLib/templates/tracker.html @@ -20,7 +20,7 @@ Multistream - This tracker will automatically update itself periodically. + Clicking on a slot's number will bring up a slot-specific auto-tracker. This tracker will automatically update itself periodically.
{% for team, players in inventory.items() %} diff --git a/WebHostLib/tracker.py b/WebHostLib/tracker.py index 567257bf..228bf375 100644 --- a/WebHostLib/tracker.py +++ b/WebHostLib/tracker.py @@ -281,7 +281,7 @@ def get_static_room_data(room: Room): @app.route('/tracker///') @cache.memoize(timeout=60) # multisave is currently created at most every minute -def getPlayerTracker(tracker: UUID, tracked_team: int, tracked_player: int): +def getPlayerTracker(tracker: UUID, tracked_team: int, tracked_player: int, want_generic: bool = False): # Team and player must be positive and greater than zero if tracked_team < 0 or tracked_player < 1: abort(404) @@ -324,7 +324,7 @@ def getPlayerTracker(tracker: UUID, tracked_team: int, tracked_player: int): checks_done[location_to_area[location]] += 1 checks_done["Total"] += 1 specific_tracker = game_specific_trackers.get(games[tracked_player], None) - if specific_tracker: + if specific_tracker and not want_generic: return specific_tracker(multisave, room, locations, inventory, tracked_team, tracked_player, player_name, seed_checks_in_area, checks_done) else: @@ -332,6 +332,11 @@ def getPlayerTracker(tracker: UUID, tracked_team: int, tracked_player: int): seed_checks_in_area, checks_done) +@app.route('/generic_tracker///') +def get_generic_tracker(tracker: UUID, tracked_team: int, tracked_player: int): + return getPlayerTracker(tracker, tracked_team, tracked_player, True) + + def __renderAlttpTracker(multisave: Dict[str, Any], room: Room, locations: Dict[int, Dict[int, Tuple[int, int]]], inventory: Counter, team: int, player: int, player_name: str, seed_checks_in_area: Dict[int, Dict[str, int]], checks_done: Dict[str, int]) -> str: @@ -423,19 +428,21 @@ def __renderMinecraftTracker(multisave: Dict[str, Any], room: Room, locations: D "Fishing Rod": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/7/7f/Fishing_Rod_JE2_BE2.png", "Campfire": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/9/91/Campfire_JE2_BE2.gif", "Water Bottle": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/7/75/Water_Bottle_JE2_BE2.png", + "Spyglass": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/c/c1/Spyglass_JE2_BE1.png", "Dragon Head": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/b/b6/Dragon_Head.png", } minecraft_location_ids = { - "Story": [42073, 42080, 42081, 42023, 42082, 42027, 42039, 42085, 42002, 42009, 42010, - 42070, 42041, 42049, 42090, 42004, 42031, 42025, 42029, 42051, 42077, 42089], + "Story": [42073, 42023, 42027, 42039, 42002, 42009, 42010, 42070, + 42041, 42049, 42004, 42031, 42025, 42029, 42051, 42077], "Nether": [42017, 42044, 42069, 42058, 42034, 42060, 42066, 42076, 42064, 42071, 42021, - 42062, 42008, 42061, 42033, 42011, 42006, 42019, 42000, 42040, 42001, 42015, 42014], + 42062, 42008, 42061, 42033, 42011, 42006, 42019, 42000, 42040, 42001, 42015, 42014], "The End": [42052, 42005, 42012, 42032, 42030, 42042, 42018, 42038, 42046], - "Adventure": [42047, 42086, 42087, 42050, 42059, 42055, 42072, 42003, 42035, 42016, 42020, - 42048, 42054, 42068, 42043, 42074, 42075, 42024, 42026, 42037, 42045, 42056, 42088], - "Husbandry": [42065, 42067, 42078, 42022, 42007, 42079, 42013, 42028, - 42036, 42057, 42063, 42053, 42083, 42084, 42091] + "Adventure": [42047, 42050, 42096, 42097, 42098, 42059, 42055, 42072, 42003, 42035, 42016, 42020, + 42048, 42054, 42068, 42043, 42074, 42075, 42024, 42026, 42037, 42045, 42056, 42099, 42100], + "Husbandry": [42065, 42067, 42078, 42022, 42007, 42079, 42013, 42028, 42036, + 42057, 42063, 42053, 42102, 42101, 42092, 42093, 42094, 42095], + "Archipelago": [42080, 42081, 42082, 42083, 42084, 42085, 42086, 42087, 42088, 42089, 42090, 42091], } display_data = {} @@ -767,6 +774,106 @@ def __renderTimespinnerTracker(multisave: Dict[str, Any], room: Room, locations: checks_done=checks_done, checks_in_area=checks_in_area, location_info=location_info, **display_data) +def __renderSuperMetroidTracker(multisave: Dict[str, Any], room: Room, locations: Dict[int, Dict[int, Tuple[int, int]]], + inventory: Counter, team: int, player: int, playerName: str, + seed_checks_in_area: Dict[int, Dict[str, int]], checks_done: Dict[str, int]) -> str: + + icons = { + "Energy Tank": "https://randommetroidsolver.pythonanywhere.com/solver/static/images/ETank.png", + "Missile": "https://randommetroidsolver.pythonanywhere.com/solver/static/images/Missile.png", + "Super Missile": "https://randommetroidsolver.pythonanywhere.com/solver/static/images/Super.png", + "Power Bomb": "https://randommetroidsolver.pythonanywhere.com/solver/static/images/PowerBomb.png", + "Bomb": "https://randommetroidsolver.pythonanywhere.com/solver/static/images/Bomb.png", + "Charge Beam": "https://randommetroidsolver.pythonanywhere.com/solver/static/images/Charge.png", + "Ice Beam": "https://randommetroidsolver.pythonanywhere.com/solver/static/images/Ice.png", + "Hi-Jump Boots": "https://randommetroidsolver.pythonanywhere.com/solver/static/images/HiJump.png", + "Speed Booster": "https://randommetroidsolver.pythonanywhere.com/solver/static/images/SpeedBooster.png", + "Wave Beam": "https://randommetroidsolver.pythonanywhere.com/solver/static/images/Wave.png", + "Spazer": "https://randommetroidsolver.pythonanywhere.com/solver/static/images/Spazer.png", + "Spring Ball": "https://randommetroidsolver.pythonanywhere.com/solver/static/images/SpringBall.png", + "Varia Suit": "https://randommetroidsolver.pythonanywhere.com/solver/static/images/Varia.png", + "Plasma Beam": "https://randommetroidsolver.pythonanywhere.com/solver/static/images/Plasma.png", + "Grappling Beam": "https://randommetroidsolver.pythonanywhere.com/solver/static/images/Grapple.png", + "Morph Ball": "https://randommetroidsolver.pythonanywhere.com/solver/static/images/Morph.png", + "Reserve Tank": "https://randommetroidsolver.pythonanywhere.com/solver/static/images/Reserve.png", + "Gravity Suit": "https://randommetroidsolver.pythonanywhere.com/solver/static/images/Gravity.png", + "X-Ray Scope": "https://randommetroidsolver.pythonanywhere.com/solver/static/images/XRayScope.png", + "Space Jump": "https://randommetroidsolver.pythonanywhere.com/solver/static/images/SpaceJump.png", + "Screw Attack": "https://randommetroidsolver.pythonanywhere.com/solver/static/images/ScrewAttack.png", + "Nothing": "", + "No Energy": "", + "Kraid": "", + "Phantoon": "", + "Draygon": "", + "Ridley": "", + "Mother Brain": "", + } + + multi_items = { + "Energy Tank": 83000, + "Missile": 83001, + "Super Missile": 83002, + "Power Bomb": 83003, + "Reserve Tank": 83020, + } + + supermetroid_location_ids = { + 'Crateria/Blue Brinstar': [82005, 82007, 82008, 82026, 82029, + 82000, 82004, 82006, 82009, 82010, + 82011, 82012, 82027, 82028, 82034, + 82036, 82037], + 'Green/Pink Brinstar': [82017, 82023, 82030, 82033, 82035, + 82013, 82014, 82015, 82016, 82018, + 82019, 82021, 82022, 82024, 82025, + 82031], + 'Red Brinstar': [82038, 82042, 82039, 82040, 82041], + 'Kraid': [82043, 82048, 82044], + 'Norfair': [82050, 82053, 82061, 82066, 82068, + 82049, 82051, 82054, 82055, 82056, + 82062, 82063, 82064, 82065, 82067], + 'Lower Norfair': [82078, 82079, 82080, 82070, 82071, + 82073, 82074, 82075, 82076, 82077], + 'Crocomire': [82052, 82060, 82057, 82058, 82059], + 'Wrecked Ship': [82129, 82132, 82134, 82135, 82001, + 82002, 82003, 82128, 82130, 82131, + 82133], + 'West Maridia': [82138, 82136, 82137, 82139, 82140, + 82141, 82142], + 'East Maridia': [82143, 82145, 82150, 82152, 82154, + 82144, 82146, 82147, 82148, 82149, + 82151], + } + + display_data = {} + + + for item_name, item_id in multi_items.items(): + base_name = item_name.split()[0].lower() + count = inventory[item_id] + display_data[base_name+"_count"] = inventory[item_id] + + # Victory condition + game_state = multisave.get("client_game_state", {}).get((team, player), 0) + display_data['game_finished'] = game_state == 30 + + # Turn location IDs into advancement tab counts + checked_locations = multisave.get("location_checks", {}).get((team, player), set()) + lookup_name = lambda id: lookup_any_location_id_to_name[id] + location_info = {tab_name: {lookup_name(id): (id in checked_locations) for id in tab_locations} + for tab_name, tab_locations in supermetroid_location_ids.items()} + checks_done = {tab_name: len([id for id in tab_locations if id in checked_locations]) + for tab_name, tab_locations in supermetroid_location_ids.items()} + checks_done['Total'] = len(checked_locations) + checks_in_area = {tab_name: len(tab_locations) for tab_name, tab_locations in supermetroid_location_ids.items()} + checks_in_area['Total'] = sum(checks_in_area.values()) + + return render_template("supermetroidTracker.html", + inventory=inventory, icons=icons, + acquired_items={lookup_any_item_id_to_name[id] for id in inventory if + id in lookup_any_item_id_to_name}, + player=player, team=team, room=room, player_name=playerName, + checks_done=checks_done, checks_in_area=checks_in_area, location_info=location_info, + **display_data) def __renderGenericTracker(multisave: Dict[str, Any], room: Room, locations: Dict[int, Dict[int, Tuple[int, int]]], inventory: Counter, team: int, player: int, playerName: str, @@ -880,5 +987,6 @@ game_specific_trackers: typing.Dict[str, typing.Callable] = { "Minecraft": __renderMinecraftTracker, "Ocarina of Time": __renderOoTTracker, "Timespinner": __renderTimespinnerTracker, - "A Link to the Past": __renderAlttpTracker + "A Link to the Past": __renderAlttpTracker, + "Super Metroid": __renderSuperMetroidTracker } \ No newline at end of file diff --git a/data/client.kv b/data/client.kv index 200ba024..09be0188 100644 --- a/data/client.kv +++ b/data/client.kv @@ -32,6 +32,8 @@ pos: (0, 0) : size: self.texture_size + size_hint: None, None + font_size: dp(18) pos_hint: {'center_y': 0.5, 'center_x': 0.5} halign: "left" canvas.before: @@ -39,4 +41,14 @@ rgba: 0.2, 0.2, 0.2, 1 Rectangle: size: self.size - pos: self.pos \ No newline at end of file + pos: self.pos + Color: + rgba: 0.098, 0.337, 0.431, 1 + Line: + width: 3 + rectangle: self.x-2, self.y-2, self.width+4, self.height+4 + Color: + rgba: 0.235, 0.678, 0.843, 1 + Line: + width: 1 + rectangle: self.x-2, self.y-2, self.width+4, self.height+4 \ No newline at end of file diff --git a/docs/network protocol.md b/docs/network protocol.md index 4673b5b7..21c7d23d 100644 --- a/docs/network protocol.md +++ b/docs/network protocol.md @@ -349,12 +349,21 @@ class JSONMessagePart(TypedDict): `type` is used to denote the intent of the message part. This can be used to indicate special information which may be rendered differently depending on client. How these types are displayed in Archipelago's ALttP client is not the end-all be-all. Other clients may choose to interpret and display these messages differently. Possible values for `type` include: -* player_id -* item_id -* location_id -* entrance_name -`color` is used to denote a console color to display the message part with. This is limited to console colors due to backwards compatibility needs with games such as ALttP. Although background colors as well as foreground colors are listed, only one may be applied to a [JSONMessagePart](#JSONMessagePart) at a time. +| Name | Notes | +| ---- | ----- | +| text | Regular text content. Is the default type and as such may be omitted. | +| player_id | player ID of someone on your team, should be resolved to Player Name | +| player_name | Player Name, could be a player within a multiplayer game or from another team, not ID resolvable | +| item_id | Item ID, should be resolved to Item Name | +| item_name | Item Name, not currently used over network, but supported by reference Clients. | +| location_id | Location ID, should be resolved to Location Name | +| location_name |Location Name, not currently used over network, but supported by reference Clients. | +| entrance_name | Entrance Name. No ID mapping exists. | +| color | Regular text that should be colored. Only `type` that will contain `color` data. | + + +`color` is used to denote a console color to display the message part with and is only send if the `type` is `color`. This is limited to console colors due to backwards compatibility needs with games such as ALttP. Although background colors as well as foreground colors are listed, only one may be applied to a [JSONMessagePart](#JSONMessagePart) at a time. Color options: * bold diff --git a/inno_setup_310.iss b/inno_setup_310.iss index 9bde5ef2..0de58e66 100644 --- a/inno_setup_310.iss +++ b/inno_setup_310.iss @@ -62,6 +62,7 @@ Name: "client/sni/sm"; Description: "SNI Client - Super Metroid Patch Setup"; Name: "client/factorio"; Description: "Factorio"; Types: full playing Name: "client/minecraft"; Description: "Minecraft"; Types: full playing; ExtraDiskSpaceRequired: 226894278 Name: "client/oot"; Description: "Ocarina of Time Adjuster"; Types: full playing +Name: "client/ff1"; Description: "Final Fantasy 1"; Types: full playing Name: "client/text"; Description: "Text, to !command and chat"; Types: full playing [Dirs] @@ -84,11 +85,9 @@ Source: "{#sourcepath}\ArchipelagoSNIClient.exe"; DestDir: "{app}"; Flags: ignor Source: "{#sourcepath}\ArchipelagoLttPAdjuster.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/sni/lttp or generator/lttp Source: "{#sourcepath}\ArchipelagoMinecraftClient.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/minecraft Source: "{#sourcepath}\ArchipelagoOoTAdjuster.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/oot +Source: "{#sourcepath}\ArchipelagoFF1Client.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/ff1 Source: "vc_redist.x64.exe"; DestDir: {tmp}; Flags: deleteafterinstall -;minecraft temp files -Source: "{tmp}\forge-installer.jar"; DestDir: "{app}"; Flags: skipifsourcedoesntexist external deleteafterinstall; Components: client/minecraft - [Icons] Name: "{group}\{#MyAppName} Folder"; Filename: "{app}"; Name: "{group}\{#MyAppName} Server"; Filename: "{app}\{#MyAppExeName}"; Components: server @@ -96,16 +95,19 @@ Name: "{group}\{#MyAppName} Text Client"; Filename: "{app}\ArchipelagoTextClient Name: "{group}\{#MyAppName} SNI Client"; Filename: "{app}\ArchipelagoSNIClient.exe"; Components: client/sni Name: "{group}\{#MyAppName} Factorio Client"; Filename: "{app}\ArchipelagoFactorioClient.exe"; Components: client/factorio Name: "{group}\{#MyAppName} Minecraft Client"; Filename: "{app}\ArchipelagoMinecraftClient.exe"; Components: client/minecraft +Name: "{group}\{#MyAppName} Final Fantasy 1 Client"; Filename: "{app}\ArchipelagoFF1Client.exe"; Components: client/ff1 + Name: "{commondesktop}\{#MyAppName} Folder"; Filename: "{app}"; Tasks: desktopicon Name: "{commondesktop}\{#MyAppName} Server"; Filename: "{app}\{#MyAppExeName}"; Tasks: desktopicon; Components: server Name: "{commondesktop}\{#MyAppName} SNI Client"; Filename: "{app}\ArchipelagoSNIClient.exe"; Tasks: desktopicon; Components: client/sni Name: "{commondesktop}\{#MyAppName} Factorio Client"; Filename: "{app}\ArchipelagoFactorioClient.exe"; Tasks: desktopicon; Components: client/factorio +Name: "{commondesktop}\{#MyAppName} Final Fantasy 1 Client"; Filename: "{app}\ArchipelagoFF1Client.exe"; Tasks: desktopicon; Components: client/factorio [Run] Filename: "{tmp}\vc_redist.x64.exe"; Parameters: "/passive /norestart"; Check: IsVCRedist64BitNeeded; StatusMsg: "Installing VC++ redistributable..." Filename: "{app}\ArchipelagoLttPAdjuster"; Parameters: "--update_sprites"; StatusMsg: "Updating Sprite Library..."; Components: client/sni/lttp or generator/lttp -Filename: "{app}\jre8\bin\java.exe"; Parameters: "-jar ""{app}\forge-installer.jar"" --installServer ""{app}\Minecraft Forge server"""; Flags: runhidden; Check: IsForgeNeeded(); StatusMsg: "Installing Forge Server..."; Components: client/minecraft +Filename: "{app}\ArchipelagoMinecraftClient.exe"; Parameters: "--install"; StatusMsg: "Installing Forge Server..."; Components: client/minecraft [UninstallDelete] Type: dirifempty; Name: "{app}" @@ -125,6 +127,11 @@ Root: HKCR; Subkey: "{#MyAppName}smpatch"; ValueData: "Archi Root: HKCR; Subkey: "{#MyAppName}smpatch\DefaultIcon"; ValueData: "{app}\ArchipelagoSNIClient.exe,0"; ValueType: string; ValueName: ""; Components: client/sni Root: HKCR; Subkey: "{#MyAppName}smpatch\shell\open\command"; ValueData: """{app}\ArchipelagoSNIClient.exe"" ""%1"""; ValueType: string; ValueName: ""; Components: client/sni +Root: HKCR; Subkey: ".apsoe"; ValueData: "{#MyAppName}soepatch"; Flags: uninsdeletevalue; ValueType: string; ValueName: ""; Components: client/sni +Root: HKCR; Subkey: "{#MyAppName}soepatch"; ValueData: "Archipelago Secret of Evermore Patch"; Flags: uninsdeletekey; ValueType: string; ValueName: ""; Components: client/sni +Root: HKCR; Subkey: "{#MyAppName}soepatch\DefaultIcon"; ValueData: "{app}\ArchipelagoSNIClient.exe,0"; ValueType: string; ValueName: ""; Components: client/sni +Root: HKCR; Subkey: "{#MyAppName}soepatch\shell\open\command"; ValueData: """{app}\ArchipelagoSNIClient.exe"" ""%1"""; + Root: HKCR; Subkey: ".apmc"; ValueData: "{#MyAppName}mcdata"; Flags: uninsdeletevalue; ValueType: string; ValueName: ""; Components: client/minecraft Root: HKCR; Subkey: "{#MyAppName}mcdata"; ValueData: "Archipelago Minecraft Data"; Flags: uninsdeletekey; ValueType: string; ValueName: ""; Components: client/minecraft Root: HKCR; Subkey: "{#MyAppName}mcdata\DefaultIcon"; ValueData: "{app}\ArchipelagoMinecraftClient.exe,0"; ValueType: string; ValueName: ""; Components: client/minecraft @@ -140,7 +147,6 @@ Root: HKCR; Subkey: "{#MyAppName}multidata\shell\open\command"; ValueData: """{ const SHCONTCH_NOPROGRESSBOX = 4; SHCONTCH_RESPONDYESTOALL = 16; - FORGE_VERSION = '1.16.5-36.2.0'; // See: https://stackoverflow.com/a/51614652/2287576 function IsVCRedist64BitNeeded(): boolean; @@ -162,48 +168,6 @@ begin end; end; -function IsForgeNeeded(): boolean; -begin - Result := True; - if (FileExists(ExpandConstant('{app}')+'\Minecraft Forge Server\forge-'+FORGE_VERSION+'.jar')) then - Result := False; -end; - -function IsJavaNeeded(): boolean; -begin - Result := True; - if (FileExists(ExpandConstant('{app}')+'\jre8\bin\java.exe')) then - Result := False; -end; - -function OnDownloadMinecraftProgress(const Url, FileName: String; const Progress, ProgressMax: Int64): Boolean; -begin - if Progress = ProgressMax then - Log(Format('Successfully downloaded Minecraft additional files to {tmp}: %s', [FileName])); - Result := True; -end; - -procedure UnZip(ZipPath, TargetPath: string); -var - Shell: Variant; - ZipFile: Variant; - TargetFolder: Variant; -begin - Shell := CreateOleObject('Shell.Application'); - - ZipFile := Shell.NameSpace(ZipPath); - if VarIsClear(ZipFile) then - RaiseException( - Format('ZIP file "%s" does not exist or cannot be opened', [ZipPath])); - - TargetFolder := Shell.NameSpace(TargetPath); - if VarIsClear(TargetFolder) then - RaiseException(Format('Target path "%s" does not exist', [TargetPath])); - - TargetFolder.CopyHere( - ZipFile.Items, SHCONTCH_NOPROGRESSBOX or SHCONTCH_RESPONDYESTOALL); -end; - var R : longint; var lttprom: string; @@ -218,8 +182,6 @@ var SoERomFilePage: TInputFileWizardPage; var ootrom: string; var OoTROMFilePage: TInputFileWizardPage; -var MinecraftDownloadPage: TDownloadWizardPage; - function GetSNESMD5OfFile(const rom: string): string; var data: AnsiString; begin @@ -267,11 +229,6 @@ begin '.sfc'); end; -procedure AddMinecraftDownloads(); -begin - MinecraftDownloadPage := CreateDownloadPage(SetupMessage(msgWizardPreparing), SetupMessage(msgPreparingDesc), @OnDownloadMinecraftProgress); -end; - procedure AddOoTRomPage(); begin ootrom := FileSearch('The Legend of Zelda - Ocarina of Time.z64', WizardDirValue()); @@ -304,33 +261,7 @@ end; function NextButtonClick(CurPageID: Integer): Boolean; begin - if (CurPageID = wpReady) and (WizardIsComponentSelected('client/minecraft')) then begin - MinecraftDownloadPage.Clear; - if(IsForgeNeeded()) then - MinecraftDownloadPage.Add('https://maven.minecraftforge.net/net/minecraftforge/forge/'+FORGE_VERSION+'/forge-'+FORGE_VERSION+'-installer.jar','forge-installer.jar',''); - if(IsJavaNeedeD()) then - MinecraftDownloadPage.Add('https://corretto.aws/downloads/latest/amazon-corretto-8-x64-windows-jre.zip','java.zip',''); - MinecraftDownloadPage.Show; - try - try - MinecraftDownloadPage.Download; - Result := True; - except - if MinecraftDownloadPage.AbortedByUser then - Log('Aborted by user.') - else - SuppressibleMsgBox(AddPeriod(GetExceptionMessage), mbCriticalError, MB_OK, IDOK); - Result := False; - end; - finally - if( isJavaNeeded() ) then - if(ForceDirectories(ExpandConstant('{app}'))) then - UnZip(ExpandConstant('{tmp}')+'\java.zip',ExpandConstant('{app}')); - MinecraftDownloadPage.Hide; - end; - Result := True; - end - else if (assigned(LttPROMFilePage)) and (CurPageID = LttPROMFilePage.ID) then + if (assigned(LttPROMFilePage)) and (CurPageID = LttPROMFilePage.ID) then Result := not (LttPROMFilePage.Values[0] = '') else if (assigned(SMROMFilePage)) and (CurPageID = SMROMFilePage.ID) then Result := not (SMROMFilePage.Values[0] = '') @@ -394,7 +325,7 @@ function GetOoTROMPath(Param: string): string; begin if Length(ootrom) > 0 then Result := ootrom - else if (Assigned(OoTROMFilePage)) then + else if Assigned(OoTROMFilePage) then begin R := CompareStr(GetMD5OfFile(OoTROMFilePage.Values[0]), '5bd1fe107bf8106b2ab6650abecd54d6') * CompareStr(GetMD5OfFile(OoTROMFilePage.Values[0]), '6697768a7a7df2dd27a692a2638ea90b') * CompareStr(GetMD5OfFile(OoTROMFilePage.Values[0]), '05f0f3ebacbc8df9243b6148ffe4792f'); if R <> 0 then @@ -421,8 +352,6 @@ begin soerom := CheckRom('Secret of Evermore (USA).sfc', '6e9c94511d04fac6e0a1e582c170be3a'); if Length(soerom) = 0 then SoEROMFilePage:= AddRomPage('Secret of Evermore (USA).sfc'); - - AddMinecraftDownloads(); end; @@ -437,4 +366,4 @@ begin Result := not (WizardIsComponentSelected('generator/soe')); if (assigned(OoTROMFilePage)) and (PageID = OoTROMFilePage.ID) then Result := not (WizardIsComponentSelected('generator/oot')); -end; +end; \ No newline at end of file diff --git a/inno_setup_38.iss b/inno_setup_38.iss index f4e751af..6818f6ed 100644 --- a/inno_setup_38.iss +++ b/inno_setup_38.iss @@ -62,6 +62,7 @@ Name: "client/sni/sm"; Description: "SNI Client - Super Metroid Patch Setup"; Name: "client/factorio"; Description: "Factorio"; Types: full playing Name: "client/minecraft"; Description: "Minecraft"; Types: full playing; ExtraDiskSpaceRequired: 226894278 Name: "client/oot"; Description: "Ocarina of Time Adjuster"; Types: full playing +Name: "client/ff1"; Description: "Final Fantasy 1"; Types: full playing Name: "client/text"; Description: "Text, to !command and chat"; Types: full playing [Dirs] @@ -84,11 +85,9 @@ Source: "{#sourcepath}\ArchipelagoSNIClient.exe"; DestDir: "{app}"; Flags: ignor Source: "{#sourcepath}\ArchipelagoLttPAdjuster.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/sni/lttp or generator/lttp Source: "{#sourcepath}\ArchipelagoMinecraftClient.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/minecraft Source: "{#sourcepath}\ArchipelagoOoTAdjuster.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/oot +Source: "{#sourcepath}\ArchipelagoFF1Client.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/ff1 Source: "vc_redist.x64.exe"; DestDir: {tmp}; Flags: deleteafterinstall -;minecraft temp files -Source: "{tmp}\forge-installer.jar"; DestDir: "{app}"; Flags: skipifsourcedoesntexist external deleteafterinstall; Components: client/minecraft - [Icons] Name: "{group}\{#MyAppName} Folder"; Filename: "{app}"; Name: "{group}\{#MyAppName} Server"; Filename: "{app}\{#MyAppExeName}"; Components: server @@ -96,16 +95,19 @@ Name: "{group}\{#MyAppName} Text Client"; Filename: "{app}\ArchipelagoTextClient Name: "{group}\{#MyAppName} SNI Client"; Filename: "{app}\ArchipelagoSNIClient.exe"; Components: client/sni Name: "{group}\{#MyAppName} Factorio Client"; Filename: "{app}\ArchipelagoFactorioClient.exe"; Components: client/factorio Name: "{group}\{#MyAppName} Minecraft Client"; Filename: "{app}\ArchipelagoMinecraftClient.exe"; Components: client/minecraft +Name: "{group}\{#MyAppName} Final Fantasy 1 Client"; Filename: "{app}\ArchipelagoFF1Client.exe"; Components: client/ff1 + Name: "{commondesktop}\{#MyAppName} Folder"; Filename: "{app}"; Tasks: desktopicon Name: "{commondesktop}\{#MyAppName} Server"; Filename: "{app}\{#MyAppExeName}"; Tasks: desktopicon; Components: server Name: "{commondesktop}\{#MyAppName} SNI Client"; Filename: "{app}\ArchipelagoSNIClient.exe"; Tasks: desktopicon; Components: client/sni Name: "{commondesktop}\{#MyAppName} Factorio Client"; Filename: "{app}\ArchipelagoFactorioClient.exe"; Tasks: desktopicon; Components: client/factorio +Name: "{commondesktop}\{#MyAppName} Final Fantasy 1 Client"; Filename: "{app}\ArchipelagoFF1Client.exe"; Tasks: desktopicon; Components: client/factorio [Run] Filename: "{tmp}\vc_redist.x64.exe"; Parameters: "/passive /norestart"; Check: IsVCRedist64BitNeeded; StatusMsg: "Installing VC++ redistributable..." Filename: "{app}\ArchipelagoLttPAdjuster"; Parameters: "--update_sprites"; StatusMsg: "Updating Sprite Library..."; Components: client/sni/lttp or generator/lttp -Filename: "{app}\jre8\bin\java.exe"; Parameters: "-jar ""{app}\forge-installer.jar"" --installServer ""{app}\Minecraft Forge server"""; Flags: runhidden; Check: IsForgeNeeded(); StatusMsg: "Installing Forge Server..."; Components: client/minecraft +Filename: "{app}\ArchipelagoMinecraftClient.exe"; Parameters: "--install"; StatusMsg: "Installing Forge Server..."; Components: client/minecraft [UninstallDelete] Type: dirifempty; Name: "{app}" @@ -125,6 +127,11 @@ Root: HKCR; Subkey: "{#MyAppName}smpatch"; ValueData: "Archi Root: HKCR; Subkey: "{#MyAppName}smpatch\DefaultIcon"; ValueData: "{app}\ArchipelagoSNIClient.exe,0"; ValueType: string; ValueName: ""; Components: client/sni Root: HKCR; Subkey: "{#MyAppName}smpatch\shell\open\command"; ValueData: """{app}\ArchipelagoSNIClient.exe"" ""%1"""; ValueType: string; ValueName: ""; Components: client/sni +Root: HKCR; Subkey: ".apsoe"; ValueData: "{#MyAppName}soepatch"; Flags: uninsdeletevalue; ValueType: string; ValueName: ""; Components: client/sni +Root: HKCR; Subkey: "{#MyAppName}soepatch"; ValueData: "Archipelago Secret of Evermore Patch"; Flags: uninsdeletekey; ValueType: string; ValueName: ""; Components: client/sni +Root: HKCR; Subkey: "{#MyAppName}soepatch\DefaultIcon"; ValueData: "{app}\ArchipelagoSNIClient.exe,0"; ValueType: string; ValueName: ""; Components: client/sni +Root: HKCR; Subkey: "{#MyAppName}soepatch\shell\open\command"; ValueData: """{app}\ArchipelagoSNIClient.exe"" ""%1"""; + Root: HKCR; Subkey: ".apmc"; ValueData: "{#MyAppName}mcdata"; Flags: uninsdeletevalue; ValueType: string; ValueName: ""; Components: client/minecraft Root: HKCR; Subkey: "{#MyAppName}mcdata"; ValueData: "Archipelago Minecraft Data"; Flags: uninsdeletekey; ValueType: string; ValueName: ""; Components: client/minecraft Root: HKCR; Subkey: "{#MyAppName}mcdata\DefaultIcon"; ValueData: "{app}\ArchipelagoMinecraftClient.exe,0"; ValueType: string; ValueName: ""; Components: client/minecraft @@ -140,7 +147,6 @@ Root: HKCR; Subkey: "{#MyAppName}multidata\shell\open\command"; ValueData: """{ const SHCONTCH_NOPROGRESSBOX = 4; SHCONTCH_RESPONDYESTOALL = 16; - FORGE_VERSION = '1.16.5-36.2.0'; // See: https://stackoverflow.com/a/51614652/2287576 function IsVCRedist64BitNeeded(): boolean; @@ -162,48 +168,6 @@ begin end; end; -function IsForgeNeeded(): boolean; -begin - Result := True; - if (FileExists(ExpandConstant('{app}')+'\Minecraft Forge Server\forge-'+FORGE_VERSION+'.jar')) then - Result := False; -end; - -function IsJavaNeeded(): boolean; -begin - Result := True; - if (FileExists(ExpandConstant('{app}')+'\jre8\bin\java.exe')) then - Result := False; -end; - -function OnDownloadMinecraftProgress(const Url, FileName: String; const Progress, ProgressMax: Int64): Boolean; -begin - if Progress = ProgressMax then - Log(Format('Successfully downloaded Minecraft additional files to {tmp}: %s', [FileName])); - Result := True; -end; - -procedure UnZip(ZipPath, TargetPath: string); -var - Shell: Variant; - ZipFile: Variant; - TargetFolder: Variant; -begin - Shell := CreateOleObject('Shell.Application'); - - ZipFile := Shell.NameSpace(ZipPath); - if VarIsClear(ZipFile) then - RaiseException( - Format('ZIP file "%s" does not exist or cannot be opened', [ZipPath])); - - TargetFolder := Shell.NameSpace(TargetPath); - if VarIsClear(TargetFolder) then - RaiseException(Format('Target path "%s" does not exist', [TargetPath])); - - TargetFolder.CopyHere( - ZipFile.Items, SHCONTCH_NOPROGRESSBOX or SHCONTCH_RESPONDYESTOALL); -end; - var R : longint; var lttprom: string; @@ -218,8 +182,6 @@ var SoERomFilePage: TInputFileWizardPage; var ootrom: string; var OoTROMFilePage: TInputFileWizardPage; -var MinecraftDownloadPage: TDownloadWizardPage; - function GetSNESMD5OfFile(const rom: string): string; var data: AnsiString; begin @@ -267,11 +229,6 @@ begin '.sfc'); end; -procedure AddMinecraftDownloads(); -begin - MinecraftDownloadPage := CreateDownloadPage(SetupMessage(msgWizardPreparing), SetupMessage(msgPreparingDesc), @OnDownloadMinecraftProgress); -end; - procedure AddOoTRomPage(); begin ootrom := FileSearch('The Legend of Zelda - Ocarina of Time.z64', WizardDirValue()); @@ -304,33 +261,7 @@ end; function NextButtonClick(CurPageID: Integer): Boolean; begin - if (CurPageID = wpReady) and (WizardIsComponentSelected('client/minecraft')) then begin - MinecraftDownloadPage.Clear; - if(IsForgeNeeded()) then - MinecraftDownloadPage.Add('https://maven.minecraftforge.net/net/minecraftforge/forge/'+FORGE_VERSION+'/forge-'+FORGE_VERSION+'-installer.jar','forge-installer.jar',''); - if(IsJavaNeedeD()) then - MinecraftDownloadPage.Add('https://corretto.aws/downloads/latest/amazon-corretto-8-x64-windows-jre.zip','java.zip',''); - MinecraftDownloadPage.Show; - try - try - MinecraftDownloadPage.Download; - Result := True; - except - if MinecraftDownloadPage.AbortedByUser then - Log('Aborted by user.') - else - SuppressibleMsgBox(AddPeriod(GetExceptionMessage), mbCriticalError, MB_OK, IDOK); - Result := False; - end; - finally - if( isJavaNeeded() ) then - if(ForceDirectories(ExpandConstant('{app}'))) then - UnZip(ExpandConstant('{tmp}')+'\java.zip',ExpandConstant('{app}')); - MinecraftDownloadPage.Hide; - end; - Result := True; - end - else if (assigned(LttPROMFilePage)) and (CurPageID = LttPROMFilePage.ID) then + if (assigned(LttPROMFilePage)) and (CurPageID = LttPROMFilePage.ID) then Result := not (LttPROMFilePage.Values[0] = '') else if (assigned(SMROMFilePage)) and (CurPageID = SMROMFilePage.ID) then Result := not (SMROMFilePage.Values[0] = '') @@ -351,7 +282,7 @@ begin R := CompareStr(GetSNESMD5OfFile(LttPROMFilePage.Values[0]), '03a63945398191337e896e5771f77173') if R <> 0 then MsgBox('ALttP ROM validation failed. Very likely wrong file.', mbInformation, MB_OK); - + Result := LttPROMFilePage.Values[0] end else @@ -399,7 +330,7 @@ begin R := CompareStr(GetMD5OfFile(OoTROMFilePage.Values[0]), '5bd1fe107bf8106b2ab6650abecd54d6') * CompareStr(GetMD5OfFile(OoTROMFilePage.Values[0]), '6697768a7a7df2dd27a692a2638ea90b') * CompareStr(GetMD5OfFile(OoTROMFilePage.Values[0]), '05f0f3ebacbc8df9243b6148ffe4792f'); if R <> 0 then MsgBox('OoT ROM validation failed. Very likely wrong file.', mbInformation, MB_OK); - + Result := OoTROMFilePage.Values[0] end else @@ -407,7 +338,7 @@ begin end; procedure InitializeWizard(); -begin +begin AddOoTRomPage(); lttprom := CheckRom('Zelda no Densetsu - Kamigami no Triforce (Japan).sfc', '03a63945398191337e896e5771f77173'); @@ -421,8 +352,6 @@ begin soerom := CheckRom('Secret of Evermore (USA).sfc', '6e9c94511d04fac6e0a1e582c170be3a'); if Length(soerom) = 0 then SoEROMFilePage:= AddRomPage('Secret of Evermore (USA).sfc'); - - AddMinecraftDownloads(); end; diff --git a/kvui.py b/kvui.py index 73ef26de..cd8da14d 100644 --- a/kvui.py +++ b/kvui.py @@ -8,11 +8,16 @@ os.environ["KIVY_NO_FILELOG"] = "1" os.environ["KIVY_NO_ARGS"] = "1" os.environ["KIVY_LOG_ENABLE"] = "0" +from kivy.base import Config +Config.set("input", "mouse", "mouse,disable_multitouch") +Config.set('kivy', 'exit_on_escape', '0') +Config.set('graphics', 'multisamples', '0') # multisamples crash old intel drivers + from kivy.app import App from kivy.core.window import Window from kivy.core.clipboard import Clipboard from kivy.core.text.markup import MarkupLabel -from kivy.base import ExceptionHandler, ExceptionManager, Config, Clock +from kivy.base import ExceptionHandler, ExceptionManager, Clock from kivy.factory import Factory from kivy.properties import BooleanProperty, ObjectProperty from kivy.uix.button import Button @@ -207,7 +212,7 @@ class GameManager(App): # keep track of last used command to autofill on click self.last_autofillable_command = "hint" - autofillable_commands = ("hint", "getitem") + autofillable_commands = ("hint_location", "hint", "getitem") original_say = ctx.on_user_say def intercept_say(text): @@ -365,6 +370,13 @@ class TextManager(GameManager): base_title = "Archipelago Text Client" +class FF1Manager(GameManager): + logging_pairs = [ + ("Client", "Archipelago") + ] + base_title = "Archipelago Final Fantasy 1 Client" + + class LogtoUI(logging.Handler): def __init__(self, on_log): super(LogtoUI, self).__init__(logging.INFO) @@ -424,6 +436,4 @@ class KivyJSONtoTextParser(JSONtoTextParser): ExceptionManager.add_handler(E()) -Config.set("input", "mouse", "mouse,disable_multitouch") -Config.set('kivy', 'exit_on_escape', '0') Builder.load_file(Utils.local_path("data", "client.kv")) diff --git a/setup.py b/setup.py index 0911ce5e..c72846b1 100644 --- a/setup.py +++ b/setup.py @@ -141,12 +141,12 @@ for folder in sdl2.dep_bins + glew.dep_bins: shutil.copytree(folder, libfolder, dirs_exist_ok=True) print('copying', folder, '->', libfolder) -extra_data = ["LICENSE", "data", "EnemizerCLI", "host.yaml", "SNI", "meta.yaml"] +extra_data = ["LICENSE", "data", "EnemizerCLI", "host.yaml", "SNI"] for data in extra_data: installfile(Path(data)) -os.makedirs(buildfolder / "Players", exist_ok=True) +os.makedirs(buildfolder / "Players" / "Templates", exist_ok=True) from WebHostLib.options import create create() from worlds.AutoWorld import AutoWorldRegister @@ -154,7 +154,8 @@ for worldname, worldtype in AutoWorldRegister.world_types.items(): if not worldtype.hidden: file_name = worldname+".yaml" shutil.copyfile(os.path.join("WebHostLib", "static", "generated", "configs", file_name), - buildfolder / "Players" / file_name) + buildfolder / "Players" / "Templates" / file_name) +shutil.copyfile("meta.yaml", buildfolder / "Players" / "Templates" / "meta.yaml") try: from maseya import z3pr diff --git a/test/minecraft/TestAdvancements.py b/test/minecraft/TestAdvancements.py index a3578973..6ddebcbf 100644 --- a/test/minecraft/TestAdvancements.py +++ b/test/minecraft/TestAdvancements.py @@ -613,19 +613,24 @@ class TestAdvancements(TestMinecraft): ["You Need a Mint", False, [], ['Progressive Resource Crafting']], ["You Need a Mint", False, [], ['Flint and Steel']], ["You Need a Mint", False, [], ['Progressive Tools']], - ["You Need a Mint", False, ['Progressive Weapons'], ['Progressive Weapons', 'Progressive Weapons']], - ["You Need a Mint", False, [], ['Progressive Armor']], + ["You Need a Mint", False, [], ['Progressive Weapons']], + ["You Need a Mint", False, [], ['Progressive Armor', 'Shield']], ["You Need a Mint", False, [], ['Brewing']], + ["You Need a Mint", False, [], ['Bottles']], ["You Need a Mint", False, ['Progressive Tools', 'Progressive Tools'], ['Bucket', 'Progressive Tools']], ["You Need a Mint", False, ['3 Ender Pearls', '3 Ender Pearls', '3 Ender Pearls'], ['3 Ender Pearls']], - ["You Need a Mint", False, [], ['Archery']], - ["You Need a Mint", False, [], ['Bottles']], - ["You Need a Mint", True, ['Progressive Resource Crafting', 'Progressive Tools', 'Flint and Steel', 'Bucket', - 'Progressive Weapons', 'Progressive Weapons', 'Archery', 'Progressive Armor', - 'Brewing', '3 Ender Pearls', '3 Ender Pearls', '3 Ender Pearls', '3 Ender Pearls', 'Bottles']], + ["You Need a Mint", True, ['Progressive Resource Crafting', 'Progressive Tools', 'Flint and Steel', 'Bucket', + 'Progressive Weapons', 'Progressive Armor', 'Brewing', + '3 Ender Pearls', '3 Ender Pearls', '3 Ender Pearls', '3 Ender Pearls', 'Bottles']], ["You Need a Mint", True, ['Progressive Resource Crafting', 'Progressive Tools', 'Flint and Steel', 'Progressive Tools', 'Progressive Tools', - 'Progressive Weapons', 'Progressive Weapons', 'Archery', 'Progressive Armor', - 'Brewing', '3 Ender Pearls', '3 Ender Pearls', '3 Ender Pearls', '3 Ender Pearls', 'Bottles']], + 'Progressive Weapons', 'Progressive Armor', 'Brewing', + '3 Ender Pearls', '3 Ender Pearls', '3 Ender Pearls', '3 Ender Pearls', 'Bottles']], + ["You Need a Mint", True, ['Progressive Resource Crafting', 'Progressive Tools', 'Flint and Steel', 'Bucket', + 'Progressive Weapons', 'Shield', 'Brewing', + '3 Ender Pearls', '3 Ender Pearls', '3 Ender Pearls', '3 Ender Pearls', 'Bottles']], + ["You Need a Mint", True, ['Progressive Resource Crafting', 'Progressive Tools', 'Flint and Steel', 'Progressive Tools', 'Progressive Tools', + 'Progressive Weapons', 'Shield', 'Brewing', + '3 Ender Pearls', '3 Ender Pearls', '3 Ender Pearls', '3 Ender Pearls', 'Bottles']], ]) def test_42047(self): @@ -954,7 +959,11 @@ class TestAdvancements(TestMinecraft): def test_42072(self): self.run_location_tests([ - ["A Throwaway Joke", True, []], + ["A Throwaway Joke", False, []], + ["A Throwaway Joke", False, [], ['Progressive Weapons']], + ["A Throwaway Joke", False, [], ['Campfire', 'Progressive Resource Crafting']], + ["A Throwaway Joke", True, ['Progressive Weapons', 'Campfire']], + ["A Throwaway Joke", True, ['Progressive Weapons', 'Progressive Resource Crafting']], ]) def test_42073(self): @@ -1143,3 +1152,127 @@ class TestAdvancements(TestMinecraft): ["Overpowered", True, ['Progressive Resource Crafting', 'Progressive Tools', 'Progressive Tools', 'Flint and Steel', 'Bucket', 'Progressive Weapons', 'Shield']], ]) + def test_42092(self): + self.run_location_tests([ + ["Wax On", False, []], + ["Wax On", False, [], ["Progressive Tools"]], + ["Wax On", False, [], ["Campfire"]], + ["Wax On", False, ["Progressive Resource Crafting"], ["Progressive Resource Crafting"]], + ["Wax On", True, ["Progressive Tools", "Progressive Resource Crafting", "Progressive Resource Crafting", "Campfire"]], + ]) + + def test_42093(self): + self.run_location_tests([ + ["Wax Off", False, []], + ["Wax Off", False, [], ["Progressive Tools"]], + ["Wax Off", False, [], ["Campfire"]], + ["Wax Off", False, ["Progressive Resource Crafting"], ["Progressive Resource Crafting"]], + ["Wax Off", True, ["Progressive Tools", "Progressive Resource Crafting", "Progressive Resource Crafting", "Campfire"]], + ]) + + def test_42094(self): + self.run_location_tests([ + ["The Cutest Predator", False, []], + ["The Cutest Predator", False, [], ["Progressive Tools"]], + ["The Cutest Predator", False, [], ["Progressive Resource Crafting"]], + ["The Cutest Predator", False, [], ["Bucket"]], + ["The Cutest Predator", True, ["Progressive Tools", "Progressive Resource Crafting", "Bucket"]], + ]) + + def test_42095(self): + self.run_location_tests([ + ["The Healing Power of Friendship", False, []], + ["The Healing Power of Friendship", False, [], ["Progressive Tools"]], + ["The Healing Power of Friendship", False, [], ["Progressive Resource Crafting"]], + ["The Healing Power of Friendship", False, [], ["Bucket"]], + ["The Healing Power of Friendship", True, ["Progressive Tools", "Progressive Resource Crafting", "Bucket"]], + ]) + + def test_42096(self): + self.run_location_tests([ + ["Is It a Bird?", False, []], + ["Is It a Bird?", False, [], ["Progressive Weapons"]], + ["Is It a Bird?", False, [], ["Progressive Tools"]], + ["Is It a Bird?", False, [], ["Progressive Resource Crafting"]], + ["Is It a Bird?", False, [], ["Spyglass"]], + ["Is It a Bird?", True, ["Progressive Weapons", "Progressive Tools", "Progressive Resource Crafting", "Spyglass"]], + ]) + + def test_42097(self): + self.run_location_tests([ + ["Is It a Balloon?", False, []], + ["Is It a Balloon?", False, [], ['Progressive Resource Crafting']], + ["Is It a Balloon?", False, [], ['Flint and Steel']], + ["Is It a Balloon?", False, [], ['Progressive Tools']], + ["Is It a Balloon?", False, [], ['Progressive Weapons']], + ["Is It a Balloon?", False, [], ['Spyglass']], + ["Is It a Balloon?", False, ['Progressive Tools', 'Progressive Tools'], ['Bucket', 'Progressive Tools']], + ["Is It a Balloon?", True, ['Progressive Resource Crafting', 'Progressive Tools', 'Flint and Steel', 'Bucket', 'Progressive Weapons', 'Spyglass']], + ["Is It a Balloon?", True, ['Progressive Resource Crafting', 'Progressive Tools', 'Flint and Steel', 'Progressive Tools', 'Progressive Tools', 'Progressive Weapons', 'Spyglass']], + ]) + + def test_42098(self): + self.run_location_tests([ + ["Is It a Plane?", False, []], + ["Is It a Plane?", False, [], ['Progressive Resource Crafting']], + ["Is It a Plane?", False, [], ['Flint and Steel']], + ["Is It a Plane?", False, [], ['Progressive Tools']], + ["Is It a Plane?", False, [], ['Progressive Weapons']], + ["Is It a Plane?", False, [], ['Progressive Armor', 'Shield']], + ["Is It a Plane?", False, [], ['Brewing']], + ["Is It a Plane?", False, ['Progressive Tools', 'Progressive Tools'], ['Bucket', 'Progressive Tools']], + ["Is It a Plane?", False, ['3 Ender Pearls', '3 Ender Pearls', '3 Ender Pearls'], ['3 Ender Pearls']], + ["Is It a Plane?", False, [], ['Spyglass']], + ["Is It a Plane?", True, ['Progressive Resource Crafting', 'Progressive Tools', 'Flint and Steel', 'Bucket', + 'Progressive Weapons', 'Progressive Armor', 'Brewing', + '3 Ender Pearls', '3 Ender Pearls', '3 Ender Pearls', '3 Ender Pearls', 'Spyglass']], + ["Is It a Plane?", True, ['Progressive Resource Crafting', 'Progressive Tools', 'Flint and Steel', 'Progressive Tools', 'Progressive Tools', + 'Progressive Weapons', 'Progressive Armor', 'Brewing', + '3 Ender Pearls', '3 Ender Pearls', '3 Ender Pearls', '3 Ender Pearls', 'Spyglass']], + ["Is It a Plane?", True, ['Progressive Resource Crafting', 'Progressive Tools', 'Flint and Steel', 'Bucket', + 'Progressive Weapons', 'Shield', 'Brewing', + '3 Ender Pearls', '3 Ender Pearls', '3 Ender Pearls', '3 Ender Pearls', 'Spyglass']], + ["Is It a Plane?", True, ['Progressive Resource Crafting', 'Progressive Tools', 'Flint and Steel', 'Progressive Tools', 'Progressive Tools', + 'Progressive Weapons', 'Shield', 'Brewing', + '3 Ender Pearls', '3 Ender Pearls', '3 Ender Pearls', '3 Ender Pearls', 'Spyglass']], + ]) + + def test_42099(self): + self.run_location_tests([ + ["Surge Protector", False, []], + ["Surge Protector", False, [], ['Channeling Book']], + ["Surge Protector", False, ['Progressive Resource Crafting'], ['Progressive Resource Crafting']], + ["Surge Protector", False, [], ['Enchanting']], + ["Surge Protector", False, [], ['Progressive Tools']], + ["Surge Protector", False, [], ['Progressive Weapons']], + ["Surge Protector", True, ['Progressive Weapons', 'Progressive Tools', 'Progressive Tools', 'Progressive Tools', + 'Enchanting', 'Progressive Resource Crafting', 'Progressive Resource Crafting', 'Channeling Book']], + ]) + + def test_42100(self): + self.run_location_tests([ + ["Light as a Rabbit", False, []], + ["Light as a Rabbit", False, [], ["Progressive Weapons"]], + ["Light as a Rabbit", False, [], ["Progressive Tools"]], + ["Light as a Rabbit", False, [], ["Progressive Resource Crafting"]], + ["Light as a Rabbit", False, [], ["Bucket"]], + ["Light as a Rabbit", True, ["Progressive Weapons", "Progressive Tools", "Progressive Resource Crafting", "Bucket"]], + ]) + + def test_42101(self): + self.run_location_tests([ + ["Glow and Behold!", False, []], + ["Glow and Behold!", False, [], ["Progressive Weapons"]], + ["Glow and Behold!", False, [], ["Progressive Resource Crafting", "Campfire"]], + ["Glow and Behold!", True, ["Progressive Weapons", "Progressive Resource Crafting"]], + ["Glow and Behold!", True, ["Progressive Weapons", "Campfire"]], + ]) + + def test_42102(self): + self.run_location_tests([ + ["Whatever Floats Your Goat!", False, []], + ["Whatever Floats Your Goat!", False, [], ["Progressive Weapons"]], + ["Whatever Floats Your Goat!", False, [], ["Progressive Resource Crafting", "Campfire"]], + ["Whatever Floats Your Goat!", True, ["Progressive Weapons", "Progressive Resource Crafting"]], + ["Whatever Floats Your Goat!", True, ["Progressive Weapons", "Campfire"]], + ]) diff --git a/test/minecraft/TestMinecraft.py b/test/minecraft/TestMinecraft.py index fd2c0c1c..2e558421 100644 --- a/test/minecraft/TestMinecraft.py +++ b/test/minecraft/TestMinecraft.py @@ -4,7 +4,7 @@ from BaseClasses import MultiWorld from worlds import AutoWorld from worlds.minecraft import MinecraftWorld from worlds.minecraft.Items import MinecraftItem, item_table -from worlds.minecraft.Options import AdvancementGoal, CombatDifficulty, BeeTraps +from worlds.minecraft.Options import * from Options import Toggle, Range # Converts the name of an item into an item object @@ -30,16 +30,17 @@ class TestMinecraft(TestBase): self.world = MultiWorld(1) self.world.game[1] = "Minecraft" self.world.worlds[1] = MinecraftWorld(self.world, 1) - exclusion_pools = ['hard', 'insane', 'postgame'] + exclusion_pools = ['hard', 'unreasonable', 'postgame'] for pool in exclusion_pools: - setattr(self.world, f"include_{pool}_advancements", [False, False]) + setattr(self.world, f"include_{pool}_advancements", {1: False}) setattr(self.world, "advancement_goal", {1: AdvancementGoal(30)}) - setattr(self.world, "shuffle_structures", {1: Toggle(False)}) - setattr(self.world, "combat_difficulty", {1: CombatDifficulty(1)}) # normal + setattr(self.world, "egg_shards_required", {1: EggShardsRequired(0)}) + setattr(self.world, "egg_shards_available", {1: EggShardsAvailable(0)}) + setattr(self.world, "required_bosses", {1: BossGoal(1)}) # ender dragon + setattr(self.world, "shuffle_structures", {1: ShuffleStructures(False)}) setattr(self.world, "bee_traps", {1: BeeTraps(0)}) + setattr(self.world, "combat_difficulty", {1: CombatDifficulty(1)}) # normal setattr(self.world, "structure_compasses", {1: Toggle(False)}) - setattr(self.world, "egg_shards_required", {1: Range(0)}) - setattr(self.world, "egg_shards_available", {1: Range(0)}) AutoWorld.call_single(self.world, "create_regions", 1) AutoWorld.call_single(self.world, "generate_basic", 1) AutoWorld.call_single(self.world, "set_rules", 1) diff --git a/worlds/alttp/EntranceShuffle.py b/worlds/alttp/EntranceShuffle.py index 2d555930..fb903f88 100644 --- a/worlds/alttp/EntranceShuffle.py +++ b/worlds/alttp/EntranceShuffle.py @@ -1796,6 +1796,7 @@ def link_inverted_entrances(world, player): if world.get_entrance('Inverted Ganons Tower', player).connected_region.name != 'Ganons Tower (Entrance)': world.ganonstower_vanilla[player] = False + def connect_simple(world, exitname, regionname, player): world.get_entrance(exitname, player).connect(world.get_region(regionname, player)) @@ -1820,6 +1821,7 @@ def connect_entrance(world, entrancename: str, exitname: str, player: int): entrance.connect(region, addresses, target) world.spoiler.set_entrance(entrance.name, exit.name if exit is not None else region.name, 'entrance', player) + def connect_exit(world, exitname, entrancename, player): entrance = world.get_entrance(entrancename, player) exit = world.get_entrance(exitname, player) diff --git a/worlds/alttp/Rom.py b/worlds/alttp/Rom.py index c5d7680b..1ac4d99c 100644 --- a/worlds/alttp/Rom.py +++ b/worlds/alttp/Rom.py @@ -2287,7 +2287,7 @@ def write_strings(rom, world, player): if hint_count: locations = world.find_items_in_locations(set(items_to_hint), player) local_random.shuffle(locations) - for x in range(hint_count): + for x in range(min(hint_count, len(locations))): this_location = locations.pop() this_hint = this_location.item.hint_text + ' can be found ' + hint_text(this_location) + '.' tt[hint_locations.pop(0)] = this_hint diff --git a/worlds/factorio/Mod.py b/worlds/factorio/Mod.py index 46d0a455..0b10e8f5 100644 --- a/worlds/factorio/Mod.py +++ b/worlds/factorio/Mod.py @@ -109,9 +109,12 @@ def generate_mod(world, output_directory: str): progressive_technology_table.values()}, "custom_recipes": world.custom_recipes, "max_science_pack": multiworld.max_science_pack[player].value, - "liquids": liquids} + "liquids": liquids, + "goal": multiworld.goal[player].value} for factorio_option in Options.factorio_options: + if factorio_option in ["free_sample_blacklist", "free_sample_whitelist"]: + continue template_data[factorio_option] = getattr(multiworld, factorio_option)[player].value if getattr(multiworld, "silo")[player].value == Options.Silo.option_randomize_recipe: @@ -120,6 +123,9 @@ def generate_mod(world, output_directory: str): if getattr(multiworld, "satellite")[player].value == Options.Satellite.option_randomize_recipe: template_data["free_sample_blacklist"]["satellite"] = 1 + template_data["free_sample_blacklist"].update({item: 1 for item in multiworld.free_sample_blacklist[player].value}) + template_data["free_sample_blacklist"].update({item: 0 for item in multiworld.free_sample_whitelist[player].value}) + control_code = control_template.render(**template_data) data_template_code = data_template.render(**template_data) data_final_fixes_code = data_final_template.render(**template_data) diff --git a/worlds/factorio/Options.py b/worlds/factorio/Options.py index abce73c5..b8b06cb2 100644 --- a/worlds/factorio/Options.py +++ b/worlds/factorio/Options.py @@ -1,7 +1,7 @@ from __future__ import annotations import typing -from Options import Choice, OptionDict, ItemDict, Option, DefaultOnToggle, Range, DeathLink +from Options import Choice, OptionDict, OptionSet, ItemDict, Option, DefaultOnToggle, Range, DeathLink from schema import Schema, Optional, And, Or # schema helpers @@ -33,6 +33,14 @@ class MaxSciencePack(Choice): return self.get_ordered_science_packs()[self.value].replace("_", "-") +class Goal(Choice): + """Goal required to complete the game.""" + displayname = "Goal" + option_rocket = 0 + option_satellite = 1 + default = 0 + + class TechCost(Choice): """How expensive are the technologies.""" displayname = "Technology Cost Scale" @@ -86,6 +94,8 @@ class TechTreeLayout(Choice): option_small_funnels = 7 option_medium_funnels = 8 option_large_funnels = 9 + option_trees = 10 + option_choices = 11 default = 0 @@ -146,6 +156,15 @@ class FactorioStartItems(ItemDict): default = {"burner-mining-drill": 19, "stone-furnace": 19} +class FactorioFreeSampleBlacklist(OptionSet): + displayname = "Free Sample Blacklist" + + +class FactorioFreeSampleWhitelist(OptionSet): + """overrides any free sample blacklist present. This may ruin the balance of the mod, be forewarned.""" + displayname = "Free Sample Whitelist" + + class TrapCount(Range): range_end = 4 @@ -306,6 +325,7 @@ class ImportedBlueprint(DefaultOnToggle): factorio_options: typing.Dict[str, type(Option)] = { "max_science_pack": MaxSciencePack, + "goal": Goal, "tech_tree_layout": TechTreeLayout, "tech_cost": TechCost, "silo": Silo, @@ -313,6 +333,8 @@ factorio_options: typing.Dict[str, type(Option)] = { "free_samples": FreeSamples, "tech_tree_information": TechTreeInformation, "starting_items": FactorioStartItems, + "free_sample_blacklist": FactorioFreeSampleBlacklist, + "free_sample_whitelist": FactorioFreeSampleWhitelist, "recipe_time": RecipeTime, "recipe_ingredients": RecipeIngredients, "imported_blueprints": ImportedBlueprint, diff --git a/worlds/factorio/Shapes.py b/worlds/factorio/Shapes.py index 12e9f0b6..eaf44ac1 100644 --- a/worlds/factorio/Shapes.py +++ b/worlds/factorio/Shapes.py @@ -1,4 +1,5 @@ from typing import Dict, List, Set +from collections import deque from worlds.factorio.Options import TechTreeLayout @@ -21,7 +22,9 @@ def get_shapes(factorio_world) -> Dict[str, List[str]]: tech_names.sort() world.random.shuffle(tech_names) - if layout == TechTreeLayout.option_small_diamonds: + if layout == TechTreeLayout.option_single: + pass + elif layout == TechTreeLayout.option_small_diamonds: slice_size = 4 while len(tech_names) > slice_size: slice = tech_names[:slice_size] @@ -189,6 +192,55 @@ def get_shapes(factorio_world) -> Dict[str, List[str]]: prerequisites.setdefault(tech_name, set()).update(previous_slice[i:i+2]) previous_slice = slice layer_size -= 1 + elif layout == TechTreeLayout.option_trees: + # 0 | + # 1 2 | + # 3 | + # 4 5 6 7 | + # 8 | + # 9 10 11 12 13 14 | + # 15 | + # 16 | + slice_size = 17 + while len(tech_names) > slice_size: + slice = tech_names[:slice_size] + tech_names = tech_names[slice_size:] + slice.sort(key=lambda tech_name: len(custom_technologies[tech_name].get_prior_technologies())) + + prerequisites[slice[1]] = {slice[0]} + prerequisites[slice[2]] = {slice[0]} + + prerequisites[slice[3]] = {slice[1], slice[2]} + + prerequisites[slice[4]] = {slice[3]} + prerequisites[slice[5]] = {slice[3]} + prerequisites[slice[6]] = {slice[3]} + prerequisites[slice[7]] = {slice[3]} + + prerequisites[slice[8]] = {slice[4], slice[5], slice[6], slice[7]} + + prerequisites[slice[9]] = {slice[8]} + prerequisites[slice[10]] = {slice[8]} + prerequisites[slice[11]] = {slice[8]} + prerequisites[slice[12]] = {slice[8]} + prerequisites[slice[13]] = {slice[8]} + prerequisites[slice[14]] = {slice[8]} + + prerequisites[slice[15]] = {slice[9], slice[10], slice[11], slice[12], slice[13], slice[14]} + prerequisites[slice[16]] = {slice[15]} + elif layout == TechTreeLayout.option_choices: + tech_names.sort(key=lambda tech_name: len(custom_technologies[tech_name].get_prior_technologies())) + current_choices = deque([tech_names[0]]) + tech_names = tech_names[1:] + while len(tech_names) > 1: + source = current_choices.pop() + choices = tech_names[:2] + tech_names = tech_names[2:] + for choice in choices: + prerequisites[choice] = {source} + current_choices.extendleft(choices) + else: + raise NotImplementedError(f"Layout {layout} is not implemented.") world.tech_tree_layout_prerequisites[player] = prerequisites return prerequisites diff --git a/worlds/factorio/__init__.py b/worlds/factorio/__init__.py index 54a8e953..0a12b7f2 100644 --- a/worlds/factorio/__init__.py +++ b/worlds/factorio/__init__.py @@ -11,7 +11,7 @@ from .Technologies import base_tech_table, recipe_sources, base_technology_table liquids from .Shapes import get_shapes from .Mod import generate_mod -from .Options import factorio_options, MaxSciencePack, Silo, Satellite, TechTreeInformation +from .Options import factorio_options, MaxSciencePack, Silo, Satellite, TechTreeInformation, Goal import logging @@ -142,12 +142,14 @@ class Factorio(World): Rules.add_rule(location, lambda state, locations=locations: all(state.can_reach(loc) for loc in locations)) - silo_recipe = None if self.world.silo[self.player].value == Silo.option_spawn \ - else self.custom_recipes["rocket-silo"] if "rocket-silo" in self.custom_recipes \ + silo_recipe = None + if self.world.silo[self.player] == Silo.option_spawn: + silo_recipe = self.custom_recipes["rocket-silo"] if "rocket-silo" in self.custom_recipes \ else next(iter(all_product_sources.get("rocket-silo"))) part_recipe = self.custom_recipes["rocket-part"] - satellite_recipe = None if self.world.max_science_pack[self.player].value != MaxSciencePack.option_space_science_pack \ - else self.custom_recipes["satellite"] if "satellite" in self.custom_recipes \ + satellite_recipe = None + if self.world.goal[self.player] == Goal.option_satellite: + satellite_recipe = self.custom_recipes["satellite"] if "satellite" in self.custom_recipes \ else next(iter(all_product_sources.get("satellite"))) victory_tech_names = get_rocket_requirements(silo_recipe, part_recipe, satellite_recipe) world.get_location("Rocket Launch", player).access_rule = lambda state: all(state.has(technology, player) @@ -171,7 +173,7 @@ class Factorio(World): return super(Factorio, self).collect_item(state, item, remove) def get_required_client_version(self) -> tuple: - return max((0, 1, 6), super(Factorio, self).get_required_client_version()) + return max((0, 2, 1), super(Factorio, self).get_required_client_version()) options = factorio_options @@ -339,7 +341,7 @@ class Factorio(World): needed_recipes = self.world.max_science_pack[self.player].get_allowed_packs() | {"rocket-part"} if self.world.silo[self.player] != Silo.option_spawn: needed_recipes |= {"rocket-silo"} - if self.world.max_science_pack[self.player].value == MaxSciencePack.option_space_science_pack: + if self.world.goal[self.player].value == Goal.option_satellite: needed_recipes |= {"satellite"} for recipe in needed_recipes: diff --git a/worlds/factorio/data/mod_template/control.lua b/worlds/factorio/data/mod_template/control.lua index d53a0b9c..4ef722cf 100644 --- a/worlds/factorio/data/mod_template/control.lua +++ b/worlds/factorio/data/mod_template/control.lua @@ -9,6 +9,7 @@ SEED_NAME = "{{ seed_name }}" FREE_SAMPLE_BLACKLIST = {{ dict_to_lua(free_sample_blacklist) }} TRAP_EVO_FACTOR = {{ evolution_trap_increase }} / 100 MAX_SCIENCE_PACK = {{ max_science_pack }} +GOAL = {{ goal }} ARCHIPELAGO_DEATH_LINK_SETTING = "archipelago-death-link-{{ slot_player }}-{{ seed_name }}" if settings.global[ARCHIPELAGO_DEATH_LINK_SETTING].value then @@ -136,7 +137,7 @@ script.on_event(defines.events.on_player_removed, on_player_removed) function on_rocket_launched(event) if event.rocket and event.rocket.valid and global.forcedata[event.rocket.force.name]['victory'] == 0 then - if event.rocket.get_item_count("satellite") > 0 or MAX_SCIENCE_PACK < 6 then + if event.rocket.get_item_count("satellite") > 0 or GOAL == 0 then global.forcedata[event.rocket.force.name]['victory'] = 1 dumpInfo(event.rocket.force) game.set_game_state diff --git a/worlds/factorio/data/mod_template/data-final-fixes.lua b/worlds/factorio/data/mod_template/data-final-fixes.lua index 83765380..d9b16bf5 100644 --- a/worlds/factorio/data/mod_template/data-final-fixes.lua +++ b/worlds/factorio/data/mod_template/data-final-fixes.lua @@ -25,6 +25,26 @@ template_tech.upgrade = false template_tech.effects = {} template_tech.prerequisites = {} +{%- if max_science_pack < 6 %} + technologies["space-science-pack"].effects = {} + {%- if max_science_pack == 0 %} + table.insert (technologies["automation"].effects, {type = "unlock-recipe", recipe = "satellite"}) + {%- elif max_science_pack == 1 %} + table.insert (technologies["logistic-science-pack"].effects, {type = "unlock-recipe", recipe = "satellite"}) + {%- elif max_science_pack == 2 %} + table.insert (technologies["military-science-pack"].effects, {type = "unlock-recipe", recipe = "satellite"}) + {%- elif max_science_pack == 3 %} + table.insert (technologies["chemical-science-pack"].effects, {type = "unlock-recipe", recipe = "satellite"}) + {%- elif max_science_pack == 4 %} + table.insert (technologies["production-science-pack"].effects, {type = "unlock-recipe", recipe = "satellite"}) + {%- elif max_science_pack == 5 %} + table.insert (technologies["utility-science-pack"].effects, {type = "unlock-recipe", recipe = "satellite"}) + {% endif %} +{% endif %} +{%- if silo == 2 %} + data.raw["recipe"]["rocket-silo"].enabled = true +{% endif %} + function prep_copy(new_copy, old_tech) old_tech.hidden = true local ingredient_filter = allowed_ingredients[old_tech.name] diff --git a/worlds/minecraft/Items.py b/worlds/minecraft/Items.py index b2806ab5..a176c4f7 100644 --- a/worlds/minecraft/Items.py +++ b/worlds/minecraft/Items.py @@ -56,10 +56,12 @@ item_table = { "Structure Compass (End City)": ItemData(45041, True), "Shulker Box": ItemData(45042, False), "Dragon Egg Shard": ItemData(45043, True), + "Spyglass": ItemData(45044, True), "Bee Trap (Minecraft)": ItemData(45100, False), "Blaze Rods": ItemData(None, True), - "Victory": ItemData(None, True) + "Defeat Ender Dragon": ItemData(None, True), + "Defeat Wither": ItemData(None, True), } # 33 required items @@ -87,6 +89,7 @@ required_items = { "Infinity Book": 1, "3 Ender Pearls": 4, "Saddle": 1, + "Spyglass": 1, } junk_weights = { diff --git a/worlds/minecraft/Locations.py b/worlds/minecraft/Locations.py index d1134e15..7dbf85de 100644 --- a/worlds/minecraft/Locations.py +++ b/worlds/minecraft/Locations.py @@ -108,9 +108,21 @@ advancement_table = { "Overkill": AdvData(42089, 'Nether Fortress'), "Librarian": AdvData(42090, 'Overworld'), "Overpowered": AdvData(42091, 'Bastion Remnant'), + "Wax On": AdvData(42092, 'Overworld'), + "Wax Off": AdvData(42093, 'Overworld'), + "The Cutest Predator": AdvData(42094, 'Overworld'), + "The Healing Power of Friendship": AdvData(42095, 'Overworld'), + "Is It a Bird?": AdvData(42096, 'Overworld'), + "Is It a Balloon?": AdvData(42097, 'The Nether'), + "Is It a Plane?": AdvData(42098, 'The End'), + "Surge Protector": AdvData(42099, 'Overworld'), + "Light as a Rabbit": AdvData(42100, 'Overworld'), + "Glow and Behold!": AdvData(42101, 'Overworld'), + "Whatever Floats Your Goat!": AdvData(42102, 'Overworld'), "Blaze Spawner": AdvData(None, 'Nether Fortress'), - "Ender Dragon": AdvData(None, 'The End') + "Ender Dragon": AdvData(None, 'The End'), + "Wither": AdvData(None, 'Nether Fortress'), } exclusion_table = { @@ -126,23 +138,39 @@ exclusion_table = { "Uneasy Alliance", "Cover Me in Debris", "A Complete Catalogue", + "Surge Protector", + "Light as a Rabbit", # will be normal in 1.18 }, - "insane": { + "unreasonable": { "How Did We Get Here?", "Adventuring Time", }, - "postgame": { - "Free the End", - "The Next Generation", - "The End... Again...", - "You Need a Mint", - "Monsters Hunted", +} + +def get_postgame_advancements(required_bosses): + + postgame_advancements = { + "ender_dragon": { + "Free the End", + "The Next Generation", + "The End... Again...", + "You Need a Mint", + "Monsters Hunted", + "Is It a Plane?", + }, + "wither": { + "Withering Heights", + "Bring Home the Beacon", + "Beaconator", + "A Furious Cocktail", + "How Did We Get Here?", + "Monsters Hunted", + } } -} -events_table = { - "Ender Dragon": "Victory" -} - -lookup_id_to_name: typing.Dict[int, str] = {loc_data.id: loc_name for loc_name, loc_data in advancement_table.items() if - loc_data.id} + advancements = set() + if required_bosses in {"ender_dragon", "both"}: + advancements.update(postgame_advancements["ender_dragon"]) + if required_bosses in {"wither", "both"}: + advancements.update(postgame_advancements["wither"]) + return advancements diff --git a/worlds/minecraft/Options.py b/worlds/minecraft/Options.py index 49929faf..edf6a93f 100644 --- a/worlds/minecraft/Options.py +++ b/worlds/minecraft/Options.py @@ -1,37 +1,51 @@ import typing -from Options import Choice, Option, Toggle, Range +from Options import Choice, Option, Toggle, Range, OptionList, DeathLink class AdvancementGoal(Range): - """Number of advancements required to spawn the Ender Dragon.""" + """Number of advancements required to spawn bosses.""" displayname = "Advancement Goal" range_start = 0 - range_end = 87 - default = 50 + range_end = 92 + default = 40 class EggShardsRequired(Range): - """Number of dragon egg shards to collect before the Ender Dragon will spawn.""" + """Number of dragon egg shards to collect to spawn bosses.""" displayname = "Egg Shards Required" range_start = 0 - range_end = 30 + range_end = 40 + default = 0 class EggShardsAvailable(Range): """Number of dragon egg shards available to collect.""" displayname = "Egg Shards Available" range_start = 0 - range_end = 30 + range_end = 40 + default = 0 + + +class BossGoal(Choice): + """Bosses which must be defeated to finish the game.""" + displayname = "Required Bosses" + option_none = 0 + option_ender_dragon = 1 + option_wither = 2 + option_both = 3 + default = 1 class ShuffleStructures(Toggle): """Enables shuffling of villages, outposts, fortresses, bastions, and end cities.""" displayname = "Shuffle Structures" + default = 1 class StructureCompasses(Toggle): """Adds structure compasses to the item pool, which point to the nearest indicated structure.""" displayname = "Structure Compasses" + default = 1 class BeeTraps(Range): @@ -39,6 +53,7 @@ class BeeTraps(Range): displayname = "Bee Trap Percentage" range_start = 0 range_end = 100 + default = 0 class CombatDifficulty(Choice): @@ -53,33 +68,45 @@ class CombatDifficulty(Choice): class HardAdvancements(Toggle): """Enables certain RNG-reliant or tedious advancements.""" displayname = "Include Hard Advancements" + default = 0 -class InsaneAdvancements(Toggle): +class UnreasonableAdvancements(Toggle): """Enables the extremely difficult advancements "How Did We Get Here?" and "Adventuring Time.\"""" - displayname = "Include Insane Advancements" + displayname = "Include Unreasonable Advancements" + default = 0 class PostgameAdvancements(Toggle): - """Enables advancements that require spawning and defeating the Ender Dragon.""" + """Enables advancements that require spawning and defeating the required bosses.""" displayname = "Include Postgame Advancements" + default = 0 class SendDefeatedMobs(Toggle): """Send killed mobs to other Minecraft worlds which have this option enabled.""" displayname = "Send Defeated Mobs" + default = 0 + + +class StartingItems(OptionList): + """Start with these items. Each entry should be of this format: {item: "item_name", amount: #, nbt: "nbt_string"}""" + displayname = "Starting Items" minecraft_options: typing.Dict[str, type(Option)] = { - "advancement_goal": AdvancementGoal, - "egg_shards_required": EggShardsRequired, - "egg_shards_available": EggShardsAvailable, - "shuffle_structures": ShuffleStructures, - "structure_compasses": StructureCompasses, - "bee_traps": BeeTraps, - "combat_difficulty": CombatDifficulty, - "include_hard_advancements": HardAdvancements, - "include_insane_advancements": InsaneAdvancements, - "include_postgame_advancements": PostgameAdvancements, - "send_defeated_mobs": SendDefeatedMobs, + "advancement_goal": AdvancementGoal, + "egg_shards_required": EggShardsRequired, + "egg_shards_available": EggShardsAvailable, + "required_bosses": BossGoal, + "shuffle_structures": ShuffleStructures, + "structure_compasses": StructureCompasses, + "bee_traps": BeeTraps, + "combat_difficulty": CombatDifficulty, + "include_hard_advancements": HardAdvancements, + "include_unreasonable_advancements": UnreasonableAdvancements, + "include_postgame_advancements": PostgameAdvancements, + "send_defeated_mobs": SendDefeatedMobs, + "starting_items": StartingItems, + "death_link": DeathLink, } diff --git a/worlds/minecraft/Rules.py b/worlds/minecraft/Rules.py index d3c0873f..e3adfc9d 100644 --- a/worlds/minecraft/Rules.py +++ b/worlds/minecraft/Rules.py @@ -1,5 +1,5 @@ -from ..generic.Rules import set_rule -from .Locations import exclusion_table, events_table +from ..generic.Rules import set_rule, add_rule +from .Locations import exclusion_table, get_postgame_advancements from BaseClasses import MultiWorld from ..AutoWorld import LogicMixin @@ -9,6 +9,9 @@ class MinecraftLogic(LogicMixin): def _mc_has_iron_ingots(self, player: int): return self.has('Progressive Tools', player) and self.has('Progressive Resource Crafting', player) + def _mc_has_copper_ingots(self, player: int): + return self.has('Progressive Tools', player) and self.has('Progressive Resource Crafting', player) + def _mc_has_gold_ingots(self, player: int): return self.has('Progressive Resource Crafting', player) and (self.has('Progressive Tools', player, 2) or self.can_reach('The Nether', 'Region', player)) @@ -21,6 +24,9 @@ class MinecraftLogic(LogicMixin): def _mc_has_bottle(self, player: int): return self.has('Bottles', player) and self.has('Progressive Resource Crafting', player) + def _mc_has_spyglass(self, player: int): + return self._mc_has_copper_ingots(player) and self.has('Spyglass', player) and self._mc_can_adventure(player) + def _mc_can_enchant(self, player: int): return self.has('Enchanting', player) and self._mc_has_diamond_pickaxe(player) # mine obsidian and lapis @@ -81,48 +87,32 @@ class MinecraftLogic(LogicMixin): return self._mc_fortress_loot(player) and (normal_kill or self.can_reach('The Nether', 'Region', player) or self.can_reach('The End', 'Region', player)) return self._mc_fortress_loot(player) and normal_kill + def _mc_can_respawn_ender_dragon(self, player: int): + return self.can_reach('The Nether', 'Region', player) and self.can_reach('The End', 'Region', player) and \ + self.has('Progressive Resource Crafting', player) # smelt sand into glass + def _mc_can_kill_ender_dragon(self, player: int): - # Since it is possible to kill the dragon without getting any of the advancements related to it, we need to require that it can be respawned. - respawn_dragon = self.can_reach('The Nether', 'Region', player) and self.has('Progressive Resource Crafting', player) if self._mc_combat_difficulty(player) == 'easy': - return respawn_dragon and self.has("Progressive Weapons", player, 3) and self.has("Progressive Armor", player, 2) and \ + return self.has("Progressive Weapons", player, 3) and self.has("Progressive Armor", player, 2) and \ self.has('Archery', player) and self._mc_can_brew_potions(player) and self._mc_can_enchant(player) if self._mc_combat_difficulty(player) == 'hard': - return respawn_dragon and ((self.has('Progressive Weapons', player, 2) and self.has('Progressive Armor', player)) or \ - (self.has('Progressive Weapons', player, 1) and self.has('Bed', player))) - return respawn_dragon and self.has('Progressive Weapons', player, 2) and self.has('Progressive Armor', player) and self.has('Archery', player) + return (self.has('Progressive Weapons', player, 2) and self.has('Progressive Armor', player)) or \ + (self.has('Progressive Weapons', player, 1) and self.has('Bed', player)) + return self.has('Progressive Weapons', player, 2) and self.has('Progressive Armor', player) and self.has('Archery', player) def _mc_has_structure_compass(self, entrance_name: str, player: int): if not self.world.structure_compasses[player]: return True return self.has(f"Structure Compass ({self.world.get_entrance(entrance_name, player).connected_region.name})", player) - -def set_rules(world: MultiWorld, player: int): - def reachable_locations(state): - postgame_advancements = exclusion_table['postgame'].copy() - for event in events_table.keys(): - postgame_advancements.add(event) - return [location for location in world.get_locations() if - location.player == player and - location.name not in postgame_advancements and - location.can_reach(state)] +# Sets rules on entrances and advancements that are always applied +def set_advancement_rules(world: MultiWorld, player: int): # Retrieves the appropriate structure compass for the given entrance def get_struct_compass(entrance_name): struct = world.get_entrance(entrance_name, player).connected_region.name return f"Structure Compass ({struct})" - # 92 total advancements. Goal is to complete X advancements and then Free the End. - # There are 5 advancements which cannot be included for dragon spawning (4 postgame, Free the End) - # Hence the true maximum is (92 - 5) = 87 - goal = world.advancement_goal[player] - egg_shards = min(world.egg_shards_required[player], world.egg_shards_available[player]) - can_complete = lambda state: len(reachable_locations(state)) >= goal and state.has("Dragon Egg Shard", player, egg_shards) and state.can_reach('The End', 'Region', player) and state._mc_can_kill_ender_dragon(player) - - if world.logic[player] != 'nologic': - world.completion_condition[player] = lambda state: state.has('Victory', player) - set_rule(world.get_entrance("Nether Portal", player), lambda state: state.has('Flint and Steel', player) and (state.has('Bucket', player) or state.has('Progressive Tools', player, 3)) and state._mc_has_iron_ingots(player)) @@ -133,7 +123,8 @@ def set_rules(world: MultiWorld, player: int): set_rule(world.get_entrance("Nether Structure 2", player), lambda state: state._mc_can_adventure(player) and state._mc_has_structure_compass("Nether Structure 2", player)) set_rule(world.get_entrance("The End Structure", player), lambda state: state._mc_can_adventure(player) and state._mc_has_structure_compass("The End Structure", player)) - set_rule(world.get_location("Ender Dragon", player), lambda state: can_complete(state)) + set_rule(world.get_location("Ender Dragon", player), lambda state: state._mc_can_kill_ender_dragon(player)) + set_rule(world.get_location("Wither", player), lambda state: state._mc_can_kill_wither(player)) set_rule(world.get_location("Blaze Spawner", player), lambda state: state._mc_fortress_loot(player)) set_rule(world.get_location("Who is Cutting Onions?", player), lambda state: state._mc_can_piglin_trade(player)) @@ -142,7 +133,7 @@ def set_rules(world: MultiWorld, player: int): set_rule(world.get_location("Very Very Frightening", player), lambda state: state.has("Channeling Book", player) and state._mc_can_use_anvil(player) and state._mc_can_enchant(player) and \ ((world.get_region('Village', player).entrances[0].parent_region.name != 'The End' and state.can_reach('Village', 'Region', player)) or state.can_reach('Zombie Doctor', 'Location', player))) # need villager into the overworld for lightning strike set_rule(world.get_location("Hot Stuff", player), lambda state: state.has("Bucket", player) and state._mc_has_iron_ingots(player)) - set_rule(world.get_location("Free the End", player), lambda state: can_complete(state)) + set_rule(world.get_location("Free the End", player), lambda state: state._mc_can_respawn_ender_dragon(player) and state._mc_can_kill_ender_dragon(player)) set_rule(world.get_location("A Furious Cocktail", player), lambda state: state._mc_can_brew_potions(player) and state.has("Fishing Rod", player) and # Water Breathing state.can_reach('The Nether', 'Region', player) and # Regeneration, Fire Resistance, gold nuggets @@ -154,7 +145,7 @@ def set_rules(world: MultiWorld, player: int): set_rule(world.get_location("Not Today, Thank You", player), lambda state: state.has("Shield", player) and state._mc_has_iron_ingots(player)) set_rule(world.get_location("Isn't It Iron Pick", player), lambda state: state.has("Progressive Tools", player, 2) and state._mc_has_iron_ingots(player)) set_rule(world.get_location("Local Brewery", player), lambda state: state._mc_can_brew_potions(player)) - set_rule(world.get_location("The Next Generation", player), lambda state: can_complete(state)) + set_rule(world.get_location("The Next Generation", player), lambda state: state._mc_can_kill_ender_dragon(player)) set_rule(world.get_location("Fishy Business", player), lambda state: state.has("Fishing Rod", player)) set_rule(world.get_location("Hot Tourist Destinations", player), lambda state: True) set_rule(world.get_location("This Boat Has Legs", player), lambda state: (state._mc_fortress_loot(player) or state._mc_complete_raid(player)) and @@ -188,7 +179,7 @@ def set_rules(world: MultiWorld, player: int): set_rule(world.get_location("Total Beelocation", player), lambda state: state.has("Silk Touch Book", player) and state._mc_can_use_anvil(player) and state._mc_can_enchant(player)) set_rule(world.get_location("Arbalistic", player), lambda state: state._mc_craft_crossbow(player) and state.has("Piercing IV Book", player) and state._mc_can_use_anvil(player) and state._mc_can_enchant(player)) - set_rule(world.get_location("The End... Again...", player), lambda state: can_complete(state)) + set_rule(world.get_location("The End... Again...", player), lambda state: state._mc_can_respawn_ender_dragon(player) and state._mc_can_kill_ender_dragon(player)) set_rule(world.get_location("Acquire Hardware", player), lambda state: state._mc_has_iron_ingots(player)) set_rule(world.get_location("Not Quite \"Nine\" Lives", player), lambda state: state._mc_can_piglin_trade(player) and state.has("Progressive Resource Crafting", player, 2)) set_rule(world.get_location("Cover Me With Diamonds", player), lambda state: state.has("Progressive Armor", player, 2) and state.can_reach("Diamonds!", "Location", player)) @@ -196,9 +187,10 @@ def set_rules(world: MultiWorld, player: int): set_rule(world.get_location("Hired Help", player), lambda state: state.has("Progressive Resource Crafting", player, 2) and state._mc_has_iron_ingots(player)) set_rule(world.get_location("Return to Sender", player), lambda state: True) set_rule(world.get_location("Sweet Dreams", player), lambda state: state.has("Bed", player) or state.can_reach('Village', 'Region', player)) - set_rule(world.get_location("You Need a Mint", player), lambda state: can_complete(state) and state._mc_has_bottle(player)) + set_rule(world.get_location("You Need a Mint", player), lambda state: state._mc_can_respawn_ender_dragon(player) and state._mc_has_bottle(player)) set_rule(world.get_location("Adventure", player), lambda state: True) - set_rule(world.get_location("Monsters Hunted", player), lambda state: can_complete(state) and state._mc_can_kill_wither(player) and state.has("Fishing Rod", player)) # pufferfish for Water Breathing + set_rule(world.get_location("Monsters Hunted", player), lambda state: state._mc_can_respawn_ender_dragon(player) and state._mc_can_kill_ender_dragon(player) and + state._mc_can_kill_wither(player) and state.has("Fishing Rod", player)) # pufferfish for Water Breathing set_rule(world.get_location("Enchanter", player), lambda state: state._mc_can_enchant(player)) set_rule(world.get_location("Voluntary Exile", player), lambda state: state._mc_basic_combat(player)) set_rule(world.get_location("Eye Spy", player), lambda state: state._mc_enter_stronghold(player)) @@ -224,7 +216,7 @@ def set_rules(world: MultiWorld, player: int): set_rule(world.get_location("Uneasy Alliance", player), lambda state: state._mc_has_diamond_pickaxe(player) and state.has('Fishing Rod', player)) set_rule(world.get_location("Diamonds!", player), lambda state: state.has("Progressive Tools", player, 2) and state._mc_has_iron_ingots(player)) set_rule(world.get_location("A Terrible Fortress", player), lambda state: True) # since you don't have to fight anything - set_rule(world.get_location("A Throwaway Joke", player), lambda state: True) # kill drowned + set_rule(world.get_location("A Throwaway Joke", player), lambda state: state._mc_can_adventure(player)) # kill drowned set_rule(world.get_location("Minecraft", player), lambda state: True) set_rule(world.get_location("Sticky Situation", player), lambda state: state.has("Campfire", player) and state._mc_has_bottle(player)) set_rule(world.get_location("Ol' Betsy", player), lambda state: state._mc_craft_crossbow(player)) @@ -249,3 +241,42 @@ def set_rules(world: MultiWorld, player: int): set_rule(world.get_location("Librarian", player), lambda state: state.has("Enchanting", player)) set_rule(world.get_location("Overpowered", player), lambda state: state._mc_has_iron_ingots(player) and state.has('Progressive Tools', player, 2) and state._mc_basic_combat(player)) # mine gold blocks w/ iron pick + set_rule(world.get_location("Wax On", player), lambda state: state._mc_has_copper_ingots(player) and state.has('Campfire', player) and + state.has('Progressive Resource Crafting', player, 2)) + set_rule(world.get_location("Wax Off", player), lambda state: state._mc_has_copper_ingots(player) and state.has('Campfire', player) and + state.has('Progressive Resource Crafting', player, 2)) + set_rule(world.get_location("The Cutest Predator", player), lambda state: state._mc_has_iron_ingots(player) and state.has('Bucket', player)) + set_rule(world.get_location("The Healing Power of Friendship", player), lambda state: state._mc_has_iron_ingots(player) and state.has('Bucket', player)) + set_rule(world.get_location("Is It a Bird?", player), lambda state: state._mc_has_spyglass(player) and state._mc_can_adventure(player)) + set_rule(world.get_location("Is It a Balloon?", player), lambda state: state._mc_has_spyglass(player)) + set_rule(world.get_location("Is It a Plane?", player), lambda state: state._mc_has_spyglass(player) and state._mc_can_respawn_ender_dragon(player)) + set_rule(world.get_location("Surge Protector", player), lambda state: state.has("Channeling Book", player) and state._mc_can_use_anvil(player) and state._mc_can_enchant(player) and \ + ((world.get_region('Village', player).entrances[0].parent_region.name != 'The End' and state.can_reach('Village', 'Region', player)) or state.can_reach('Zombie Doctor', 'Location', player))) + set_rule(world.get_location("Light as a Rabbit", player), lambda state: state._mc_can_adventure(player) and state._mc_has_iron_ingots(player) and state.has('Bucket', player)) + set_rule(world.get_location("Glow and Behold!", player), lambda state: state._mc_can_adventure(player)) + set_rule(world.get_location("Whatever Floats Your Goat!", player), lambda state: state._mc_can_adventure(player)) + +# Sets rules on completion condition and postgame advancements +def set_completion_rules(world: MultiWorld, player: int): + def reachable_locations(state): + postgame_advancements = get_postgame_advancements(world.required_bosses[player].current_key) + return [location for location in world.get_locations() if + location.player == player and + location.name not in postgame_advancements and + location.address != None and + location.can_reach(state)] + + def defeated_required_bosses(state): + return (world.required_bosses[player].current_key not in {"ender_dragon", "both"} or state.has("Defeat Ender Dragon", player)) and \ + (world.required_bosses[player].current_key not in {"wither", "both"} or state.has("Defeat Wither", player)) + + # 103 total advancements. Goal is to complete X advancements and then defeat the dragon. + # There are 11 possible postgame advancements; 5 for dragon, 5 for wither, 1 shared between them + # Hence the max for completion is 92 + egg_shards = min(world.egg_shards_required[player], world.egg_shards_available[player]) + completion_requirements = lambda state: len(reachable_locations(state)) >= world.advancement_goal[player] and \ + state.has("Dragon Egg Shard", player, egg_shards) + world.completion_condition[player] = lambda state: completion_requirements(state) and defeated_required_bosses(state) + # Set rules on postgame advancements + for adv_name in get_postgame_advancements(world.required_bosses[player].current_key): + add_rule(world.get_location(adv_name, player), completion_requirements) diff --git a/worlds/minecraft/__init__.py b/worlds/minecraft/__init__.py index a8a78fb9..45f7860c 100644 --- a/worlds/minecraft/__init__.py +++ b/worlds/minecraft/__init__.py @@ -4,16 +4,17 @@ from base64 import b64encode, b64decode from math import ceil from .Items import MinecraftItem, item_table, required_items, junk_weights -from .Locations import MinecraftAdvancement, advancement_table, exclusion_table, events_table +from .Locations import MinecraftAdvancement, advancement_table, exclusion_table, get_postgame_advancements from .Regions import mc_regions, link_minecraft_structures, default_connections -from .Rules import set_rules +from .Rules import set_advancement_rules, set_completion_rules from worlds.generic.Rules import exclusion_rules from BaseClasses import Region, Entrance, Item from .Options import minecraft_options from ..AutoWorld import World -client_version = 6 +client_version = 7 +minecraft_version = "1.17.1" class MinecraftWorld(World): """ @@ -29,7 +30,7 @@ class MinecraftWorld(World): item_name_to_id = {name: data.code for name, data in item_table.items()} location_name_to_id = {name: data.id for name, data in advancement_table.items()} - data_version = 3 + data_version = 4 def _get_mc_data(self): exits = [connection[0] for connection in default_connections] @@ -39,12 +40,16 @@ class MinecraftWorld(World): 'player_name': self.world.get_player_name(self.player), 'player_id': self.player, 'client_version': client_version, + 'minecraft_version': minecraft_version, 'structures': {exit: self.world.get_entrance(exit, self.player).connected_region.name for exit in exits}, 'advancement_goal': self.world.advancement_goal[self.player], 'egg_shards_required': min(self.world.egg_shards_required[self.player], self.world.egg_shards_available[self.player]), 'egg_shards_available': self.world.egg_shards_available[self.player], + 'required_bosses': self.world.required_bosses[self.player].current_key, 'MC35': bool(self.world.send_defeated_mobs[self.player]), - 'race': self.world.is_race + 'death_link': bool(self.world.death_link[self.player]), + 'starting_items': str(self.world.starting_items[self.player].value), + 'race': self.world.is_race, } def generate_basic(self): @@ -72,20 +77,24 @@ class MinecraftWorld(World): # Choose locations to automatically exclude based on settings exclusion_pool = set() - exclusion_types = ['hard', 'insane', 'postgame'] + exclusion_types = ['hard', 'unreasonable'] for key in exclusion_types: if not getattr(self.world, f"include_{key}_advancements")[self.player]: exclusion_pool.update(exclusion_table[key]) + # For postgame advancements, check with the boss goal + exclusion_pool.update(get_postgame_advancements(self.world.required_bosses[self.player].current_key)) exclusion_rules(self.world, self.player, exclusion_pool) # Prefill event locations with their events self.world.get_location("Blaze Spawner", self.player).place_locked_item(self.create_item("Blaze Rods")) - self.world.get_location("Ender Dragon", self.player).place_locked_item(self.create_item("Victory")) + self.world.get_location("Ender Dragon", self.player).place_locked_item(self.create_item("Defeat Ender Dragon")) + self.world.get_location("Wither", self.player).place_locked_item(self.create_item("Defeat Wither")) self.world.itempool += itempool def set_rules(self): - set_rules(self.world, self.player) + set_advancement_rules(self.world, self.player) + set_completion_rules(self.world, self.player) def create_regions(self): def MCRegion(region_name: str, exits=[]): @@ -110,7 +119,8 @@ class MinecraftWorld(World): slot_data = self._get_mc_data() for option_name in minecraft_options: option = getattr(self.world, option_name)[self.player] - slot_data[option_name] = int(option.value) + if slot_data.get(option_name, None) is None and type(option.value) in {str, int}: + slot_data[option_name] = int(option.value) return slot_data def create_item(self, name: str) -> Item: diff --git a/worlds/oot/Rom.py b/worlds/oot/Rom.py index b867d2ca..d4396254 100644 --- a/worlds/oot/Rom.py +++ b/worlds/oot/Rom.py @@ -282,22 +282,22 @@ class Rom(BigStream): def compress_rom_file(input_file, output_file): - subcall = [] - compressor_path = data_path("Compress") if platform.system() == 'Windows': - compressor_path += "\\Compress.exe" + executable_path = "Compress.exe" elif platform.system() == 'Linux': if platform.uname()[4] == 'aarch64' or platform.uname()[4] == 'arm64': - compressor_path += "/Compress_ARM64" + executable_path = "Compress_ARM64" else: - compressor_path += "/Compress" + executable_path = "Compress" elif platform.system() == 'Darwin': - compressor_path += "/Compress.out" + executable_path = "Compress.out" else: raise RuntimeError('Unsupported operating system for compression.') - + compressor_path = os.path.join(compressor_path, executable_path) if not os.path.exists(compressor_path): raise RuntimeError(f'Compressor does not exist! Please place it at {compressor_path}.') - process = subprocess.call([compressor_path, input_file, output_file], **subprocess_args(include_stdout=False)) + import logging + logging.info(subprocess.check_output([compressor_path, input_file, output_file], + **subprocess_args(include_stdout=False))) diff --git a/worlds/sm/Options.py b/worlds/sm/Options.py index 16ccb5d2..010e8017 100644 --- a/worlds/sm/Options.py +++ b/worlds/sm/Options.py @@ -1,5 +1,5 @@ import typing -from Options import Choice, Range, OptionDict, OptionList, Option, Toggle, DefaultOnToggle, DeathLink +from Options import Choice, Range, OptionDict, OptionList, Option, Toggle, DefaultOnToggle class StartItemsRemovesFromPool(Toggle): displayname = "StartItems Removes From Item Pool" @@ -40,6 +40,16 @@ class StartLocation(Choice): option_Golden_Four = 14 default = 1 +class DeathLink(Choice): + """When DeathLink is enabled and someone dies, you will die. With survive reserve tanks can save you.""" + displayname = "Death Link" + option_disable = 0 + option_enable = 1 + option_enable_survive = 3 + alias_false = 0 + alias_true = 1 + default = 0 + class MaxDifficulty(Choice): displayname = "Maximum Difficulty" option_easy = 0 @@ -57,9 +67,6 @@ class MorphPlacement(Choice): option_normal = 1 default = 0 -class SuitsRestriction(DefaultOnToggle): - displayname = "Suits Restriction" - class StrictMinors(Toggle): displayname = "Strict Minors" @@ -117,12 +124,15 @@ class BossRandomization(Toggle): displayname = "Boss Randomization" class FunCombat(Toggle): + """if used, might force 'items' accessibility""" displayname = "Fun Combat" class FunMovement(Toggle): + """if used, might force 'items' accessibility""" displayname = "Fun Movement" class FunSuits(Toggle): + """if used, might force 'items' accessibility""" displayname = "Fun Suits" class LayoutPatches(DefaultOnToggle): @@ -197,7 +207,7 @@ sm_options: typing.Dict[str, type(Option)] = { #"progression_speed": "medium", #"progression_difficulty": "normal", "morph_placement": MorphPlacement, - "suits_restriction": SuitsRestriction, + #"suits_restriction": SuitsRestriction, #"hide_items": "off", "strict_minors": StrictMinors, "missile_qty": MissileQty, diff --git a/worlds/sm/__init__.py b/worlds/sm/__init__.py index 005844ea..6abcbfbe 100644 --- a/worlds/sm/__init__.py +++ b/worlds/sm/__init__.py @@ -2,7 +2,7 @@ import logging import copy import os import threading -from typing import Set +from typing import Set, List logger = logging.getLogger("Super Metroid") @@ -59,7 +59,7 @@ class SMWorld(World): def sm_init(self, parent: MultiWorld): if (hasattr(parent, "state")): # for unit tests where MultiWorld is instanciated before worlds - self.smbm = {player: SMBoolManager(player, parent.state.smbm[player].maxDiff) for player in parent.get_game_players("Super Metroid")} + self.smbm = {player: SMBoolManager(player, parent.state.smbm[player].maxDiff, parent.state.smbm[player].onlyBossLeft) for player in parent.get_game_players("Super Metroid")} orig_init(self, parent) @@ -88,6 +88,10 @@ class SMWorld(World): if (self.variaRando.args.morphPlacement == "early"): self.world.local_items[self.player].value.add('Morph') + + if (len(self.variaRando.randoExec.setup.restrictedLocs) > 0): + self.world.accessibility[self.player] = self.world.accessibility[self.player].from_text("items") + logger.warning(f"accessibility forced to 'items' for player {self.world.get_player_name(self.player)} because of 'fun' settings") def generate_basic(self): itemPool = self.variaRando.container.itemPool @@ -157,6 +161,10 @@ class SMWorld(World): create_locations(self, self.player) create_regions(self, self.world, self.player) + def get_required_client_version(self): + # changes to client DeathLink handling for 0.2.1 + return max(super(SMWorld, self).get_required_client_version(), (0, 2, 1)) + def getWord(self, w): return (w & 0x00FF, (w & 0xFF00) >> 8) @@ -274,7 +282,7 @@ class SMWorld(World): openTourianGreyDoors = {0x07C823 + 5: [0x0C], 0x07C831 + 5: [0x0C]} - deathLink = {0x277f04: [int(self.world.death_link[self.player])]} + deathLink = {0x277f04: [self.world.death_link[self.player].value]} playerNames = {} playerNameIDMap = {} @@ -476,17 +484,6 @@ class SMWorld(World): item.player != self.player or item.name != "Morph Ball"] - def post_fill(self): - # increase maxDifficulty if only bosses is too difficult to beat game - new_state = CollectionState(self.world) - for item in self.world.itempool: - if item.player == self.player: - new_state.collect(item, True) - new_state.sweep_for_events() - if (any(not self.world.get_location(bossLoc, self.player).can_reach(new_state) for bossLoc in self.locked_items)): - if (self.variaRando.randoExec.setup.services.onlyBossesLeft(self.variaRando.randoExec.setup.startAP, self.variaRando.randoExec.setup.container)): - self.world.state.smbm[self.player].maxDiff = infinity - @classmethod def stage_fill_hook(cls, world, progitempool, nonexcludeditempool, localrestitempool, nonlocalrestitempool, restitempool, fill_locations): @@ -494,6 +491,22 @@ class SMWorld(World): progitempool.sort( key=lambda item: 1 if (item.name == 'Morph Ball') else 0) + def post_fill(self): + new_state = CollectionState(self.world) + progitempool = [] + for item in self.world.itempool: + if item.player == self.player and item.advancement: + progitempool.append(item) + + for item in progitempool: + new_state.collect(item, True) + + bossesLoc = ['Draygon', 'Kraid', 'Ridley', 'Phantoon', 'Mother Brain'] + for bossLoc in bossesLoc: + if (not self.world.get_location(bossLoc, self.player).can_reach(new_state)): + self.world.state.smbm[self.player].onlyBossLeft = True + break + def create_locations(self, player: int): for name, id in locations_lookup_name_to_id.items(): self.locations[name] = SMLocation(player, name, id) diff --git a/worlds/sm/variaRandomizer/graph/graph.py b/worlds/sm/variaRandomizer/graph/graph.py index 0f88678f..bcbf1381 100644 --- a/worlds/sm/variaRandomizer/graph/graph.py +++ b/worlds/sm/variaRandomizer/graph/graph.py @@ -134,9 +134,9 @@ class AccessGraph(object): def printGraph(self): if self.log.getEffectiveLevel() == logging.DEBUG: - self.log("Area graph:") + self.log.debug("Area graph:") for s, d in self.InterAreaTransitions: - self.log("{} -> {}".format(s.Name, d.Name)) + self.log.debug("{} -> {}".format(s.Name, d.Name)) def addAccessPoint(self, ap): ap.distance = 0 diff --git a/worlds/sm/variaRandomizer/logic/helpers.py b/worlds/sm/variaRandomizer/logic/helpers.py index 72d2e4be..4df46657 100644 --- a/worlds/sm/variaRandomizer/logic/helpers.py +++ b/worlds/sm/variaRandomizer/logic/helpers.py @@ -566,6 +566,8 @@ class Helpers(object): # print('RIDLEY', ammoMargin, secs) (diff, defenseItems) = self.computeBossDifficulty(ammoMargin, secs, Settings.bossesDifficulty['Ridley']) + if (sm.onlyBossLeft): + diff = 1 if diff < 0: return smboolFalse else: @@ -580,6 +582,8 @@ class Helpers(object): #print('KRAID True ', ammoMargin, secs) (diff, defenseItems) = self.computeBossDifficulty(ammoMargin, secs, Settings.bossesDifficulty['Kraid']) + if (sm.onlyBossLeft): + diff = 1 if diff < 0: return smboolFalse @@ -621,6 +625,8 @@ class Helpers(object): if sm.haveItem('Gravity') and sm.haveItem('ScrewAttack'): fight.difficulty /= Settings.algoSettings['draygonScrewBonus'] fight.difficulty = self.adjustHealthDropDiff(fight.difficulty) + if (sm.onlyBossLeft): + fight.difficulty = 1 else: fight = smboolFalse # for grapple kill considers energy drained by wall socket + 2 spankings by Dray @@ -661,6 +667,8 @@ class Helpers(object): elif not hasCharge and sm.itemCount('Missile') <= 2: # few missiles is harder difficulty *= Settings.algoSettings['phantoonLowMissileMalus'] difficulty = self.adjustHealthDropDiff(difficulty) + if (sm.onlyBossLeft): + difficulty = 1 fight = SMBool(True, difficulty, items=ammoItems+defenseItems) return sm.wor(fight, @@ -707,6 +715,8 @@ class Helpers(object): # print('MB2', ammoMargin, secs) #print("ammoMargin: {}, secs: {}, settings: {}, energyDiff: {}".format(ammoMargin, secs, Settings.bossesDifficulty['MotherBrain'], energyDiff)) (diff, defenseItems) = self.computeBossDifficulty(ammoMargin, secs, Settings.bossesDifficulty['MotherBrain'], energyDiff) + if (sm.onlyBossLeft): + diff = 1 if diff < 0: return smboolFalse return SMBool(True, diff, items=ammoItems+defenseItems) diff --git a/worlds/sm/variaRandomizer/logic/smboolmanager.py b/worlds/sm/variaRandomizer/logic/smboolmanager.py index b4ee2918..93e50424 100644 --- a/worlds/sm/variaRandomizer/logic/smboolmanager.py +++ b/worlds/sm/variaRandomizer/logic/smboolmanager.py @@ -13,12 +13,13 @@ class SMBoolManager(object): items = ['ETank', 'Missile', 'Super', 'PowerBomb', 'Bomb', 'Charge', 'Ice', 'HiJump', 'SpeedBooster', 'Wave', 'Spazer', 'SpringBall', 'Varia', 'Plasma', 'Grapple', 'Morph', 'Reserve', 'Gravity', 'XRayScope', 'SpaceJump', 'ScrewAttack', 'Nothing', 'NoEnergy', 'MotherBrain', 'Hyper'] + Bosses.Golden4() countItems = ['Missile', 'Super', 'PowerBomb', 'ETank', 'Reserve'] - def __init__(self, player=0, maxDiff=sys.maxsize): + def __init__(self, player=0, maxDiff=sys.maxsize, onlyBossLeft = False): self._items = { } self._counts = { } self.player = player self.maxDiff = maxDiff + self.onlyBossLeft = onlyBossLeft # cache related self.cacheKey = 0 diff --git a/worlds/sm/variaRandomizer/rando/ItemLocContainer.py b/worlds/sm/variaRandomizer/rando/ItemLocContainer.py index 8ddf2a87..1ab43355 100644 --- a/worlds/sm/variaRandomizer/rando/ItemLocContainer.py +++ b/worlds/sm/variaRandomizer/rando/ItemLocContainer.py @@ -76,7 +76,7 @@ class ItemLocContainer(object): locs = copy.copy(self.unusedLocations) # we don't copy restriction state on purpose: it depends on # outside context we don't want to bring to the copy - ret = ItemLocContainer(SMBoolManager(self.sm.player, self.sm.maxDiff), + ret = ItemLocContainer(SMBoolManager(self.sm.player, self.sm.maxDiff, self.sm.onlyBossLeft), self.itemPoolBackup[:] if self.itemPoolBackup != None else self.itemPool[:], locs) ret.currentItems = self.currentItems[:] @@ -103,7 +103,7 @@ class ItemLocContainer(object): # transfer collected items/locations to another container def transferCollected(self, dest): dest.currentItems = self.currentItems[:] - dest.sm = SMBoolManager(self.sm.player, self.sm.maxDiff) + dest.sm = SMBoolManager(self.sm.player, self.sm.maxDiff, self.sm.onlyBossLeft) dest.sm.addItems([item.Type for item in dest.currentItems]) dest.itemLocations = copy.copy(self.itemLocations) dest.unrestrictedItems = copy.copy(self.unrestrictedItems) diff --git a/worlds/sm/variaRandomizer/randomizer.py b/worlds/sm/variaRandomizer/randomizer.py index d87c3e39..a95764df 100644 --- a/worlds/sm/variaRandomizer/randomizer.py +++ b/worlds/sm/variaRandomizer/randomizer.py @@ -348,8 +348,7 @@ class VariaRandomizer: if response.ok: PresetLoader.factory(json.loads(response.text)).load(self.player) else: - print("Got error {} {} {} from trying to fetch varia custom preset named {}".format(response.status_code, response.reason, response.text, preset_name)) - sys.exit(-1) + raise Exception("Got error {} {} {} from trying to fetch varia custom preset named {}".format(response.status_code, response.reason, response.text, preset_name)) else: preset = 'default' PresetLoader.factory(os.path.join(appDir, getPresetDir('casual'), 'casual.json')).load(self.player) @@ -365,13 +364,11 @@ class VariaRandomizer: self.seed = args.seed logger.debug("seed: {}".format(self.seed)) - seed4rand = self.seed if args.raceMagic is not None: if args.raceMagic <= 0 or args.raceMagic >= 0x10000: print("Invalid magic") sys.exit(-1) - seed4rand = self.seed ^ args.raceMagic - # random.seed(seed4rand) + # if no max diff, set it very high if args.maxDifficulty: if args.maxDifficulty == 'random': diff --git a/worlds/sm/variaRandomizer/utils/utils.py b/worlds/sm/variaRandomizer/utils/utils.py index d64cb252..402c6299 100644 --- a/worlds/sm/variaRandomizer/utils/utils.py +++ b/worlds/sm/variaRandomizer/utils/utils.py @@ -311,7 +311,7 @@ def loadRandoPreset(world, player, args): args.animals = world.animals[player].value args.noVariaTweaks = not world.varia_tweaks[player].value args.maxDifficulty = diffs[world.max_difficulty[player].value] - args.suitsRestriction = world.suits_restriction[player].value + #args.suitsRestriction = world.suits_restriction[player].value #args.hideItems = world.hide_items[player].value args.strictMinors = world.strict_minors[player].value args.noLayout = not world.layout_patches[player].value diff --git a/worlds/soe/Options.py b/worlds/soe/Options.py index 9eddd0e7..0b986d2f 100644 --- a/worlds/soe/Options.py +++ b/worlds/soe/Options.py @@ -16,10 +16,11 @@ class EvermizerFlag: return self.flag if self.value != self.default else '' -class OffOnChaosChoice(Choice): +class OffOnFullChoice(Choice): option_off = 0 option_on = 1 - option_chaos = 2 + option_full = 2 + alias_chaos = 2 alias_false = 0 alias_true = 1 @@ -30,7 +31,8 @@ class Difficulty(EvermizerFlags, Choice): option_easy = 0 option_normal = 1 option_hard = 2 - option_chaos = 3 # random is reserved pre 0.2 + option_mystery = 3 # 'random' is reserved + alias_chaos = 3 default = 1 flags = ['e', 'n', 'h', 'x'] @@ -88,27 +90,27 @@ class ShorterDialogs(EvermizerFlag, Toggle): class ShortBossRush(EvermizerFlag, Toggle): - """Start boss rush at Magmar, cut HP in half""" + """Start boss rush at Metal Magmar, cut enemy HP in half""" displayname = "Short Boss Rush" flag = 'f' -class Ingredienizer(EvermizerFlags, OffOnChaosChoice): - """Shuffles or randomizes spell ingredients""" +class Ingredienizer(EvermizerFlags, OffOnFullChoice): + """On Shuffles, Full randomizes spell ingredients""" displayname = "Ingredienizer" default = 1 flags = ['i', '', 'I'] -class Sniffamizer(EvermizerFlags, OffOnChaosChoice): - """Shuffles or randomizes drops in sniff locations""" +class Sniffamizer(EvermizerFlags, OffOnFullChoice): + """On Shuffles, Full randomizes drops in sniff locations""" displayname = "Sniffamizer" default = 1 flags = ['s', '', 'S'] -class Callbeadamizer(EvermizerFlags, OffOnChaosChoice): - """Shuffles call bead characters or spells""" +class Callbeadamizer(EvermizerFlags, OffOnFullChoice): + """On Shuffles call bead characters, Full shuffles individual spells""" displayname = "Callbeadamizer" default = 1 flags = ['c', '', 'C'] @@ -120,8 +122,8 @@ class Musicmizer(EvermizerFlag, Toggle): flag = 'm' -class Doggomizer(EvermizerFlags, OffOnChaosChoice): - """On shuffles dog per act, Chaos randomizes dog per screen, Pupdunk gives you Everpupper everywhere""" +class Doggomizer(EvermizerFlags, OffOnFullChoice): + """On shuffles dog per act, Full randomizes dog per screen, Pupdunk gives you Everpupper everywhere""" displayname = "Doggomizer" option_pupdunk = 3 default = 0 diff --git a/worlds/soe/requirements.txt b/worlds/soe/requirements.txt index 814d6f14..b5f74f45 100644 --- a/worlds/soe/requirements.txt +++ b/worlds/soe/requirements.txt @@ -1,14 +1,14 @@ -https://github.com/black-sliver/pyevermizer/releases/download/v0.39.2/pyevermizer-0.39.2-cp38-cp38-win_amd64.whl#egg=pyevermizer; sys_platform == 'win32' and platform_machine == 'AMD64' and python_version == '3.8' -https://github.com/black-sliver/pyevermizer/releases/download/v0.39.2/pyevermizer-0.39.2-cp39-cp39-win_amd64.whl#egg=pyevermizer; sys_platform == 'win32' and platform_machine == 'AMD64' and python_version == '3.9' -https://github.com/black-sliver/pyevermizer/releases/download/v0.39.2/pyevermizer-0.39.2-cp310-cp310-win_amd64.whl#egg=pyevermizer; sys_platform == 'win32' and platform_machine == 'AMD64' and python_version == '3.10' -https://github.com/black-sliver/pyevermizer/releases/download/v0.39.2/pyevermizer-0.39.2-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl#egg=pyevermizer; sys_platform == 'linux' and platform_machine == 'x86_64' and python_version == '3.8' -https://github.com/black-sliver/pyevermizer/releases/download/v0.39.2/pyevermizer-0.39.2-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl#egg=pyevermizer; sys_platform == 'linux' and platform_machine == 'x86_64' and python_version == '3.9' -https://github.com/black-sliver/pyevermizer/releases/download/v0.39.2/pyevermizer-0.39.2-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl#egg=pyevermizer; sys_platform == 'linux' and platform_machine == 'x86_64' and python_version == '3.10' -https://github.com/black-sliver/pyevermizer/releases/download/v0.39.2/pyevermizer-0.39.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl#egg=pyevermizer; sys_platform == 'linux' and platform_machine == 'aarch64' and python_version == '3.8' -https://github.com/black-sliver/pyevermizer/releases/download/v0.39.2/pyevermizer-0.39.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl#egg=pyevermizer; sys_platform == 'linux' and platform_machine == 'aarch64' and python_version == '3.9' -https://github.com/black-sliver/pyevermizer/releases/download/v0.39.2/pyevermizer-0.39.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl#egg=pyevermizer; sys_platform == 'linux' and platform_machine == 'aarch64' and python_version == '3.10' -https://github.com/black-sliver/pyevermizer/releases/download/v0.39.2/pyevermizer-0.39.2-cp38-cp38-macosx_10_9_amd64.whl#egg=pyevermizer; sys_platform == 'darwin' and python_version == '3.8' -#https://github.com/black-sliver/pyevermizer/releases/download/v0.39.2/pyevermizer-0.39.2-cp39-cp39-macosx_10_9_amd64.whl#egg=pyevermizer; sys_platform == 'darwin' and python_version == '3.9' -https://github.com/black-sliver/pyevermizer/releases/download/v0.39.2/pyevermizer-0.39.2-cp39-cp39-macosx_10_9_universal2.whl#egg=pyevermizer; sys_platform == 'darwin' and python_version == '3.9' -https://github.com/black-sliver/pyevermizer/releases/download/v0.39.2/pyevermizer-0.39.2-cp310-cp310-macosx_10_9_universal2.whl#egg=pyevermizer; sys_platform == 'darwin' and python_version == '3.10' +https://github.com/black-sliver/pyevermizer/releases/download/v0.40.0/pyevermizer-0.40.0-cp38-cp38-win_amd64.whl#egg=pyevermizer; sys_platform == 'win32' and platform_machine == 'AMD64' and python_version == '3.8' +https://github.com/black-sliver/pyevermizer/releases/download/v0.40.0/pyevermizer-0.40.0-cp39-cp39-win_amd64.whl#egg=pyevermizer; sys_platform == 'win32' and platform_machine == 'AMD64' and python_version == '3.9' +https://github.com/black-sliver/pyevermizer/releases/download/v0.40.0/pyevermizer-0.40.0-cp310-cp310-win_amd64.whl#egg=pyevermizer; sys_platform == 'win32' and platform_machine == 'AMD64' and python_version == '3.10' +https://github.com/black-sliver/pyevermizer/releases/download/v0.40.0/pyevermizer-0.40.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl#egg=pyevermizer; sys_platform == 'linux' and platform_machine == 'x86_64' and python_version == '3.8' +https://github.com/black-sliver/pyevermizer/releases/download/v0.40.0/pyevermizer-0.40.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl#egg=pyevermizer; sys_platform == 'linux' and platform_machine == 'x86_64' and python_version == '3.9' +https://github.com/black-sliver/pyevermizer/releases/download/v0.40.0/pyevermizer-0.40.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl#egg=pyevermizer; sys_platform == 'linux' and platform_machine == 'x86_64' and python_version == '3.10' +https://github.com/black-sliver/pyevermizer/releases/download/v0.40.0/pyevermizer-0.40.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl#egg=pyevermizer; sys_platform == 'linux' and platform_machine == 'aarch64' and python_version == '3.8' +https://github.com/black-sliver/pyevermizer/releases/download/v0.40.0/pyevermizer-0.40.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl#egg=pyevermizer; sys_platform == 'linux' and platform_machine == 'aarch64' and python_version == '3.9' +https://github.com/black-sliver/pyevermizer/releases/download/v0.40.0/pyevermizer-0.40.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl#egg=pyevermizer; sys_platform == 'linux' and platform_machine == 'aarch64' and python_version == '3.10' +https://github.com/black-sliver/pyevermizer/releases/download/v0.40.0/pyevermizer-0.40.0-cp38-cp38-macosx_10_9_x86_64.whl#egg=pyevermizer; sys_platform == 'darwin' and python_version == '3.8' +#https://github.com/black-sliver/pyevermizer/releases/download/v0.40.0/pyevermizer-0.40.0-cp39-cp39-macosx_10_9_x86_64.whl#egg=pyevermizer; sys_platform == 'darwin' and python_version == '3.9' +https://github.com/black-sliver/pyevermizer/releases/download/v0.40.0/pyevermizer-0.40.0-cp39-cp39-macosx_10_9_universal2.whl#egg=pyevermizer; sys_platform == 'darwin' and python_version == '3.9' +https://github.com/black-sliver/pyevermizer/releases/download/v0.40.0/pyevermizer-0.40.0-cp310-cp310-macosx_10_9_universal2.whl#egg=pyevermizer; sys_platform == 'darwin' and python_version == '3.10' bsdiff4>=1.2.1 diff --git a/worlds/timespinner/Items.py b/worlds/timespinner/Items.py index 82958eba..78bfcd8b 100644 --- a/worlds/timespinner/Items.py +++ b/worlds/timespinner/Items.py @@ -213,6 +213,7 @@ starter_melee_weapons: Tuple[str, ...] = ( ) starter_spells: Tuple[str, ...] = ( + 'Aura Blast', 'Colossal Blade', 'Infernal Flames', 'Plasma Geyser', diff --git a/worlds/timespinner/PyramidKeys.py b/worlds/timespinner/PyramidKeys.py index 761dccb9..4e47b516 100644 --- a/worlds/timespinner/PyramidKeys.py +++ b/worlds/timespinner/PyramidKeys.py @@ -27,4 +27,7 @@ def get_pyramid_keys_unlock(world: MultiWorld, player: int) -> str: else: gates = (*past_teleportation_gates, *present_teleportation_gates) + if not world: + return gates[0] + return world.random.choice(gates) \ No newline at end of file diff --git a/worlds/timespinner/Regions.py b/worlds/timespinner/Regions.py index 5a3423fc..9db742eb 100644 --- a/worlds/timespinner/Regions.py +++ b/worlds/timespinner/Regions.py @@ -1,4 +1,4 @@ -from typing import List, Dict, Tuple, Optional, Callable +from typing import List, Set, Dict, Tuple, Optional, Callable from BaseClasses import MultiWorld, Region, Entrance, Location, RegionType from .Options import is_option_enabled from .Locations import LocationData @@ -6,7 +6,7 @@ from .Locations import LocationData def create_regions(world: MultiWorld, player: int, locations: Tuple[LocationData, ...], location_cache: List[Location], pyramid_keys_unlock: str): locations_per_region = get_locations_per_region(locations) - world.regions += [ + regions = [ create_region(world, player, locations_per_region, location_cache, 'Menu'), create_region(world, player, locations_per_region, location_cache, 'Tutorial'), create_region(world, player, locations_per_region, location_cache, 'Lake desolation'), @@ -45,6 +45,11 @@ def create_regions(world: MultiWorld, player: int, locations: Tuple[LocationData create_region(world, player, locations_per_region, location_cache, 'Space time continuum') ] + if __debug__: + throwIfAnyLocationIsNotAssignedToARegion(regions, locations_per_region.keys()) + + world.regions += regions + connectStartingRegion(world, player) names: Dict[str, int] = {} @@ -94,8 +99,8 @@ def create_regions(world: MultiWorld, player: int, locations: Tuple[LocationData connect(world, player, names, 'Skeleton Shaft', 'Sealed Caves (upper)', lambda state: state._timespinner_has_keycard_A(world, player)) connect(world, player, names, 'Skeleton Shaft', 'Space time continuum', lambda state: state.has('Twin Pyramid Key', player)) connect(world, player, names, 'Sealed Caves (upper)', 'Skeleton Shaft') - connect(world, player, names, 'Sealed Caves (upper)', 'Sealed Caves (Xarion)', lambda state: state.has('Twin Pyramid Key', player) or state._timespinner_has_forwarddash_doublejump(world, player)) - connect(world, player, names, 'Sealed Caves (Xarion)', 'Sealed Caves (upper)', lambda state: state._timespinner_has_forwarddash_doublejump(world, player)) + connect(world, player, names, 'Sealed Caves (upper)', 'Sealed Caves (Xarion)', lambda state: state.has('Twin Pyramid Key', player) or state._timespinner_has_doublejump(world, player)) + connect(world, player, names, 'Sealed Caves (Xarion)', 'Sealed Caves (upper)', lambda state: state._timespinner_has_doublejump(world, player)) connect(world, player, names, 'Sealed Caves (Xarion)', 'Space time continuum', lambda state: state.has('Twin Pyramid Key', player)) connect(world, player, names, 'Refugee Camp', 'Forest') connect(world, player, names, 'Refugee Camp', 'Library', lambda state: not is_option_enabled(world, player, "Inverted")) @@ -114,9 +119,9 @@ def create_regions(world: MultiWorld, player: int, locations: Tuple[LocationData connect(world, player, names, 'Lower Lake Serene', 'Left Side forest Caves') connect(world, player, names, 'Lower Lake Serene', 'Caves of Banishment (upper)') connect(world, player, names, 'Caves of Banishment (upper)', 'Upper Lake Serene', lambda state: state.has('Water Mask', player)) - connect(world, player, names, 'Caves of Banishment (upper)', 'Caves of Banishment (Maw)', lambda state: state.has('Twin Pyramid Key', player) or state._timespinner_has_forwarddash_doublejump(world, player)) + connect(world, player, names, 'Caves of Banishment (upper)', 'Caves of Banishment (Maw)', lambda state: state.has('Twin Pyramid Key', player) or state._timespinner_has_doublejump(world, player)) connect(world, player, names, 'Caves of Banishment (upper)', 'Space time continuum', lambda state: state.has('Twin Pyramid Key', player)) - connect(world, player, names, 'Caves of Banishment (Maw)', 'Caves of Banishment (upper)', lambda state: state._timespinner_has_forwarddash_doublejump(world, player)) + connect(world, player, names, 'Caves of Banishment (Maw)', 'Caves of Banishment (upper)', lambda state: state._timespinner_has_doublejump(world, player)) connect(world, player, names, 'Caves of Banishment (Maw)', 'Caves of Banishment (Sirens)', lambda state: state.has('Gas Mask', player)) connect(world, player, names, 'Caves of Banishment (Maw)', 'Space time continuum', lambda state: state.has('Twin Pyramid Key', player)) connect(world, player, names, 'Caves of Banishment (Sirens)', 'Forest') @@ -150,6 +155,16 @@ def create_regions(world: MultiWorld, player: int, locations: Tuple[LocationData connect(world, player, names, 'Space time continuum', 'Caves of Banishment (upper)', lambda state: pyramid_keys_unlock == "GateCavesOfBanishment") +def throwIfAnyLocationIsNotAssignedToARegion(regions: List[Region], regionNames: Set[str]): + existingRegions = set() + + for region in regions: + existingRegions.add(region.name) + + if (regionNames - existingRegions): + raise Exception("Tiemspinner: the following regions are used in locations: {}, but no such region exists".format(regionNames - existingRegions)) + + def create_location(player: int, location_data: LocationData, region: Region, location_cache: List[Location]) -> Location: location = Location(player, location_data.name, location_data.code, region) location.access_rule = location_data.rule diff --git a/worlds/timespinner/__init__.py b/worlds/timespinner/__init__.py index b0602f18..623de7c6 100644 --- a/worlds/timespinner/__init__.py +++ b/worlds/timespinner/__init__.py @@ -1,4 +1,4 @@ -from typing import Dict, List, Set, TextIO +from typing import Dict, List, Set, Tuple, TextIO from BaseClasses import Item, MultiWorld, Location from ..AutoWorld import World from .LogicMixin import TimespinnerLogic @@ -24,19 +24,31 @@ class TimespinnerWorld(World): location_name_to_id = {location.name: location.code for location in get_locations(None, None)} item_name_groups = get_item_names_per_category() - locked_locations: Dict[int, List[str]] = {} - pyramid_keys_unlock: Dict[int, str] = {} - location_cache: Dict[int, List[Location]] = {} + locked_locations: List[str] + pyramid_keys_unlock: str + location_cache: List[Location] + + def __init__(self, world: MultiWorld, player: int): + super().__init__(world, player) + + self.locked_locations = [] + self.location_cache = [] + self.pyramid_keys_unlock = get_pyramid_keys_unlock(world, player) + def generate_early(self): - self.locked_locations[self.player] = [] - self.location_cache[self.player] = [] - self.pyramid_keys_unlock[self.player] = get_pyramid_keys_unlock(self.world, self.player) + # in generate_early the start_inventory isnt copied over to precollected_items yet, so we can still modify the options directly + if self.world.start_inventory[self.player].value.pop('Meyef', 0) > 0: + self.world.StartWithMeyef[self.player].value = self.world.StartWithMeyef[self.player].option_true + if self.world.start_inventory[self.player].value.pop('Talaria Attachment', 0) > 0: + self.world.QuickSeed[self.player].value = self.world.QuickSeed[self.player].option_true + if self.world.start_inventory[self.player].value.pop('Jewelry Box', 0) > 0: + self.world.StartWithJewelryBox[self.player].value = self.world.StartWithJewelryBox[self.player].option_true def create_regions(self): create_regions(self.world, self.player, get_locations(self.world, self.player), - self.location_cache[self.player], self.pyramid_keys_unlock[self.player]) + self.location_cache, self.pyramid_keys_unlock) def create_item(self, name: str) -> Item: @@ -44,22 +56,22 @@ class TimespinnerWorld(World): def set_rules(self): - setup_events(self.world, self.player, self.locked_locations[self.player], self.location_cache[self.player]) + setup_events(self.world, self.player, self.locked_locations, self.location_cache) self.world.completion_condition[self.player] = lambda state: state.has('Killed Nightmare', self.player) def generate_basic(self): - excluded_items = get_excluded_items_based_on_options(self.world, self.player) + excluded_items = get_excluded_items(self, self.world, self.player) - assign_starter_items(self.world, self.player, excluded_items, self.locked_locations[self.player]) + assign_starter_items(self.world, self.player, excluded_items, self.locked_locations) if not is_option_enabled(self.world, self.player, "QuickSeed") and not is_option_enabled(self.world, self.player, "Inverted"): - place_first_progression_item(self.world, self.player, excluded_items, self.locked_locations[self.player]) + place_first_progression_item(self.world, self.player, excluded_items, self.locked_locations) pool = get_item_pool(self.world, self.player, excluded_items) - fill_item_pool_with_dummy_items(self.world, self.player, self.locked_locations[self.player], self.location_cache[self.player], pool) + fill_item_pool_with_dummy_items(self.world, self.player, self.locked_locations, self.location_cache, pool) self.world.itempool += pool @@ -73,17 +85,17 @@ class TimespinnerWorld(World): slot_data["StinkyMaw"] = True slot_data["ProgressiveVerticalMovement"] = False slot_data["ProgressiveKeycards"] = False - slot_data["PyramidKeysGate"] = self.pyramid_keys_unlock[self.player] - slot_data["PersonalItems"] = get_personal_items(self.player, self.location_cache[self.player]) + slot_data["PyramidKeysGate"] = self.pyramid_keys_unlock + slot_data["PersonalItems"] = get_personal_items(self.player, self.location_cache) return slot_data def write_spoiler_header(self, spoiler_handle: TextIO): - spoiler_handle.write('Twin Pyramid Keys unlock: %s\n' % (self.pyramid_keys_unlock[self.player])) - + spoiler_handle.write('Twin Pyramid Keys unlock: %s\n' % (self.pyramid_keys_unlock)) -def get_excluded_items_based_on_options(world: MultiWorld, player: int) -> Set[str]: + +def get_excluded_items(self: TimespinnerWorld, world: MultiWorld, player: int) -> Set[str]: excluded_items: Set[str] = set() if is_option_enabled(world, player, "StartWithJewelryBox"): @@ -92,25 +104,47 @@ def get_excluded_items_based_on_options(world: MultiWorld, player: int) -> Set[s excluded_items.add('Meyef') if is_option_enabled(world, player, "QuickSeed"): excluded_items.add('Talaria Attachment') + + for item in world.precollected_items[player]: + if item.name not in self.item_name_groups['UseItem']: + excluded_items.add(item.name) return excluded_items def assign_starter_items(world: MultiWorld, player: int, excluded_items: Set[str], locked_locations: List[str]): - melee_weapon = world.random.choice(starter_melee_weapons) - spell = world.random.choice(starter_spells) + non_local_items = world.non_local_items[player].value - excluded_items.add(melee_weapon) - excluded_items.add(spell) + local_starter_melee_weapons = tuple(item for item in starter_melee_weapons if item not in non_local_items) + if not local_starter_melee_weapons: + if 'Plasma Orb' in non_local_items: + raise Exception("Atleast one melee orb must be local") + else: + local_starter_melee_weapons = ('Plasma Orb',) - melee_weapon_item = create_item_with_correct_settings(world, player, melee_weapon) - spell_item = create_item_with_correct_settings(world, player, spell) + local_starter_spells = tuple(item for item in starter_spells if item not in non_local_items) + if not local_starter_spells: + if 'Lightwall' in non_local_items: + raise Exception("Atleast one spell must be local") + else: + local_starter_spells = ('Lightwall',) - world.get_location('Yo Momma 1', player).place_locked_item(melee_weapon_item) - world.get_location('Yo Momma 2', player).place_locked_item(spell_item) + assign_starter_item(world, player, excluded_items, locked_locations, 'Yo Momma 1', local_starter_melee_weapons) + assign_starter_item(world, player, excluded_items, locked_locations, 'Yo Momma 2', local_starter_spells) - locked_locations.append('Yo Momma 1') - locked_locations.append('Yo Momma 2') + +def assign_starter_item(world: MultiWorld, player: int, excluded_items: Set[str], locked_locations: List[str], + location: str, item_list: Tuple[str, ...]): + + item_name = world.random.choice(item_list) + + excluded_items.add(item_name) + + item = create_item_with_correct_settings(world, player, item_name) + + world.get_location(location, player).place_locked_item(item) + + locked_locations.append(location) def get_item_pool(world: MultiWorld, player: int, excluded_items: Set[str]) -> List[Item]: @@ -133,8 +167,20 @@ def fill_item_pool_with_dummy_items(world: MultiWorld, player: int, locked_locat def place_first_progression_item(world: MultiWorld, player: int, excluded_items: Set[str], locked_locations: List[str]): - progression_item = world.random.choice(starter_progression_items) - location = world.random.choice(starter_progression_locations) + for item in world.precollected_items[player]: + if item.name in starter_progression_items: + return + + local_starter_progression_items = tuple( + item for item in starter_progression_items if item not in world.non_local_items[player].value) + non_excluded_starter_progression_locations = tuple( + location for location in starter_progression_locations if location not in world.exclude_locations[player].value) + + if not local_starter_progression_items or not non_excluded_starter_progression_locations: + return + + progression_item = world.random.choice(local_starter_progression_items) + location = world.random.choice(non_excluded_starter_progression_locations) excluded_items.add(progression_item) locked_locations.append(location)