From 246a5c568b6b0b0d7bc6d7f8bd4d38de19935f70 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Tue, 30 Nov 2021 05:33:56 +0100 Subject: [PATCH 01/65] Core: add some more types --- BaseClasses.py | 30 +++++++++++++++--------------- Main.py | 3 +-- worlds/alttp/EntranceShuffle.py | 2 ++ 3 files changed, 18 insertions(+), 17 deletions(-) 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/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/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) From 0992087e9ad2202eb98974b149a5b0ac9de46942 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Tue, 30 Nov 2021 06:09:40 +0100 Subject: [PATCH 02/65] MultiServer: add not found to !hint response and color found text Clients: text parsing fixes --- NetUtils.py | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/NetUtils.py b/NetUtils.py index d4f90b2a..bdf01c38 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' @@ -283,9 +281,11 @@ class Hint(typing.NamedTuple): else: add_json_text(parts, "'s World") if self.found: - add_json_text(parts, ". (found)") + add_json_text(parts, ". ") + add_json_text(parts, "(found)", type="color", color="green") else: - add_json_text(parts, ".") + 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, From 520e5feefbaff56dfdde142cf83eedad9f4c5b50 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Tue, 30 Nov 2021 06:41:50 +0100 Subject: [PATCH 03/65] Docs: add missed JSONMessagePart types --- NetUtils.py | 3 +-- docs/network protocol.md | 19 ++++++++++++++----- 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/NetUtils.py b/NetUtils.py index bdf01c38..ff49b8b8 100644 --- a/NetUtils.py +++ b/NetUtils.py @@ -280,11 +280,10 @@ 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, ". ") 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", diff --git a/docs/network protocol.md b/docs/network protocol.md index 4673b5b7..1a715048 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 | +| 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 From 49a0f473cef8e8652c77e2064394b75d6a5529fa Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Tue, 30 Nov 2021 08:25:22 +0100 Subject: [PATCH 04/65] Docs: add more explanation to text type of JSONMessagePart --- docs/network protocol.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/network protocol.md b/docs/network protocol.md index 1a715048..21c7d23d 100644 --- a/docs/network protocol.md +++ b/docs/network protocol.md @@ -352,7 +352,7 @@ Possible values for `type` include: | Name | Notes | | ---- | ----- | -| text | Regular text content | +| 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 | From d7509972e4007693eedc7c87d353916bd2f4fef0 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Wed, 1 Dec 2021 01:01:41 +0100 Subject: [PATCH 05/65] SNIClient: fix apsoe handling --- Patch.py | 2 +- SNIClient.py | 3 ++- inno_setup_310.iss | 5 +++++ inno_setup_38.iss | 5 +++++ 4 files changed, 13 insertions(+), 2 deletions(-) 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/SNIClient.py b/SNIClient.py index 61262afe..ca63eb2a 100644 --- a/SNIClient.py +++ b/SNIClient.py @@ -1062,7 +1062,8 @@ 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 diff --git a/inno_setup_310.iss b/inno_setup_310.iss index 9bde5ef2..09125537 100644 --- a/inno_setup_310.iss +++ b/inno_setup_310.iss @@ -125,6 +125,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 diff --git a/inno_setup_38.iss b/inno_setup_38.iss index f4e751af..c6f9ed75 100644 --- a/inno_setup_38.iss +++ b/inno_setup_38.iss @@ -125,6 +125,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 From 3fa253bac53aac2ffd62e3160e08732baab7a531 Mon Sep 17 00:00:00 2001 From: espeon65536 <81029175+espeon65536@users.noreply.github.com> Date: Tue, 30 Nov 2021 20:37:11 -0500 Subject: [PATCH 06/65] MC: 1.17 support (#120) * MC: add death_link option * Minecraft: 1.17 advancements and logic support * Update Minecraft tracker to 1.17 * Minecraft: add tests for new advancements * removed jdk/forge download install out of iss and into MinecraftClient.py using flag --install * Add required_bosses option choices are none, ender_dragon, wither, both postgame advancements are set according to the required boss for completion * fix docstring for PostgameAdvancements * Minecraft: add starting_items List of dicts: item, amount, nbt * Update descriptions for AdvancementGoal and EggShardsRequired * Minecraft: fix tests for required_bosses attribute * Minecraft: updated logic for various dragon-related advancements Split the logic into can_respawn and can_kill dragon Free the End, Monsters Hunted, The End Again still require both respawn and kill, since the player needs to kill and be credited with the kill You Need a Mint and Is It a Plane now require only respawn, since the dragon need only be alive; if killed out of logic, it's ok The Next Generation only requires kill, since the egg spawns regardless of whether the player was credited with the kill or not * Minecraft client: ignore prereleases unless --prerelease flag is on * explicitly state all defaults change structure shuffle and structure compass defaults to true update install tutorial to point to player-settings page, as well as removing instructions for manual install * Minecraft client: add Minecraft version check Adds a minecraft_version field in the apmc, and downloads only mods which contain that version in the name of the .jar file. This ensures that the client remains compatible even if new mods are released for later versions, since they won't download a mod for a later version than the apmc says. Co-authored-by: Kono Tyran --- .gitignore | 4 + MinecraftClient.py | 164 +++++++++++++----- Options.py | 4 +- .../assets/tutorial/minecraft/minecraft_en.md | 112 +++--------- WebHostLib/templates/minecraftTracker.html | 1 + WebHostLib/tracker.py | 16 +- inno_setup_310.iss | 87 +--------- inno_setup_38.iss | 91 +--------- test/minecraft/TestAdvancements.py | 153 ++++++++++++++-- test/minecraft/TestMinecraft.py | 15 +- worlds/minecraft/Items.py | 5 +- worlds/minecraft/Locations.py | 58 +++++-- worlds/minecraft/Options.py | 70 +++++--- worlds/minecraft/Rules.py | 101 +++++++---- worlds/minecraft/__init__.py | 28 ++- 15 files changed, 501 insertions(+), 408 deletions(-) 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/MinecraftClient.py b/MinecraftClient.py index 509c70b0..e178c00b 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.0.109" def prompt_yes_no(prompt): @@ -30,15 +31,6 @@ 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') @@ -77,37 +69,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 +151,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 +222,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 +238,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 +253,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 +266,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/Options.py b/Options.py index 9fc2da5a..03fb0c84 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 diff --git a/WebHostLib/static/assets/tutorial/minecraft/minecraft_en.md b/WebHostLib/static/assets/tutorial/minecraft/minecraft_en.md index 5cb7ef82..44eeed6f 100644 --- a/WebHostLib/static/assets/tutorial/minecraft/minecraft_en.md +++ b/WebHostLib/static/assets/tutorial/minecraft/minecraft_en.md @@ -1,11 +1,9 @@ # Minecraft Randomizer Setup Guide -#Automatic Hosting Install -- download and install [Archipelago](https://github.com/ArchipelagoMW/Archipelago/releases) and choose the `Minecraft Client` module - ## Required Software -- [Minecraft Java Edition](https://www.minecraft.net/en-us/store/minecraft-java-edition) +- [Minecraft Java Edition](https://www.minecraft.net/en-us/store/minecraft-java-edition) (update 1.17.1) +- [Archipelago](https://github.com/ArchipelagoMW/Archipelago/releases) (select `Minecraft Client` during installation.) ## Configuring your YAML file @@ -16,73 +14,7 @@ each player to enjoy an experience customized for their taste, and different pla can all have different options. ### Where do I get a YAML file? -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 -``` +you can customize your settings by visiting the [minecraft player settings](/games/Minecraft/player-settings) ## Joining a MultiWorld Game @@ -93,38 +25,34 @@ When you join a multiworld game, you will be asked to provide your YAML file to 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 -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`. Once 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 Archipleago 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 +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. +forge server. to star the game once everyone is ready type `/start`. +### Useful commands +- `!help` displays a list all server commands +- `!hint` will display how many hint points you have, along with any hints that have been given that are related to your game. +- `!hint (item)` will ask the server to tell you where (item) is +- `!hint_location (location)` will ask the server to tell you what item is on (location) -## Manual Installation Procedures -this is only required if you wish to set up a forge install yourself, its recommended to just use the Archipelago Installer. -###Required Software -- [Minecraft Forge](https://files.minecraftforge.net/net/minecraftforge/forge/index_1.16.5.html) +## 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. but for those of you who know how, and wish to +do so the following links are the versions of the software we use. +### Manual install Software links +- [Minecraft Forge](https://files.minecraftforge.net/net/minecraftforge/forge/index_1.17.1.html) - [Minecraft Archipelago Randomizer Mod](https://github.com/KonoTyran/Minecraft_AP_Randomizer/releases) **DO NOT INSTALL THIS ON YOUR CLIENT** -### 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. +- [Java 16](https://docs.aws.amazon.com/corretto/latest/corretto-16-ug/downloads-list.html) -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/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/tracker.py b/WebHostLib/tracker.py index 567257bf..59102707 100644 --- a/WebHostLib/tracker.py +++ b/WebHostLib/tracker.py @@ -423,19 +423,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 = {} diff --git a/inno_setup_310.iss b/inno_setup_310.iss index 09125537..13b0ac4c 100644 --- a/inno_setup_310.iss +++ b/inno_setup_310.iss @@ -86,9 +86,6 @@ Source: "{#sourcepath}\ArchipelagoMinecraftClient.exe"; DestDir: "{app}"; Flags: Source: "{#sourcepath}\ArchipelagoOoTAdjuster.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/oot 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 @@ -105,7 +102,7 @@ Name: "{commondesktop}\{#MyAppName} Factorio Client"; Filename: "{app}\Archipela 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}" @@ -145,7 +142,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; @@ -167,48 +163,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; @@ -223,8 +177,6 @@ var SoERomFilePage: TInputFileWizardPage; var ootrom: string; var OoTROMFilePage: TInputFileWizardPage; -var MinecraftDownloadPage: TDownloadWizardPage; - function GetSNESMD5OfFile(const rom: string): string; var data: AnsiString; begin @@ -272,11 +224,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()); @@ -309,33 +256,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] = '') @@ -426,8 +347,6 @@ begin soerom := CheckRom('Secret of Evermore (USA).sfc', '6e9c94511d04fac6e0a1e582c170be3a'); if Length(soerom) = 0 then SoEROMFilePage:= AddRomPage('Secret of Evermore (USA).sfc'); - - AddMinecraftDownloads(); end; @@ -442,4 +361,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 c6f9ed75..35c8c414 100644 --- a/inno_setup_38.iss +++ b/inno_setup_38.iss @@ -86,9 +86,6 @@ Source: "{#sourcepath}\ArchipelagoMinecraftClient.exe"; DestDir: "{app}"; Flags: Source: "{#sourcepath}\ArchipelagoOoTAdjuster.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/oot 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 @@ -105,7 +102,7 @@ Name: "{commondesktop}\{#MyAppName} Factorio Client"; Filename: "{app}\Archipela 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}" @@ -145,7 +142,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; @@ -167,48 +163,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; @@ -223,8 +177,6 @@ var SoERomFilePage: TInputFileWizardPage; var ootrom: string; var OoTROMFilePage: TInputFileWizardPage; -var MinecraftDownloadPage: TDownloadWizardPage; - function GetSNESMD5OfFile(const rom: string): string; var data: AnsiString; begin @@ -272,11 +224,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()); @@ -309,33 +256,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] = '') @@ -356,7 +277,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 @@ -404,7 +325,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 @@ -412,7 +333,7 @@ begin end; procedure InitializeWizard(); -begin +begin AddOoTRomPage(); lttprom := CheckRom('Zelda no Densetsu - Kamigami no Triforce (Japan).sfc', '03a63945398191337e896e5771f77173'); @@ -426,8 +347,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/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/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..04e87c06 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,46 @@ 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" + default = 0 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: From db56f4a6b78c338e0253da7c67e416320ab9800e Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Wed, 1 Dec 2021 02:39:52 +0100 Subject: [PATCH 07/65] Core: bump version to 0.2.1 --- Utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 From b7128e6ee2b2d095042cfa9838481de81207e644 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Wed, 1 Dec 2021 02:47:08 +0100 Subject: [PATCH 08/65] FF1: add to setup --- inno_setup_310.iss | 7 ++++++- inno_setup_38.iss | 5 +++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/inno_setup_310.iss b/inno_setup_310.iss index 13b0ac4c..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,6 +85,7 @@ 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 [Icons] @@ -93,10 +95,13 @@ 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] @@ -320,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 diff --git a/inno_setup_38.iss b/inno_setup_38.iss index 35c8c414..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,6 +85,7 @@ 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 [Icons] @@ -93,10 +95,13 @@ 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] From 763edf00f24eb64ee4ed4ab184b4085ab1bee12d Mon Sep 17 00:00:00 2001 From: CaitSith2 Date: Tue, 30 Nov 2021 23:18:17 -0800 Subject: [PATCH 09/65] Satellite now a possible goal for ALL science pack levels, chosen by option. Satellite unlocks by respective science pack (or by automation in the case of automation science pack) --- worlds/factorio/Mod.py | 3 ++- worlds/factorio/Options.py | 9 +++++++++ worlds/factorio/__init__.py | 4 ++-- worlds/factorio/data/mod_template/control.lua | 3 ++- .../data/mod_template/data-final-fixes.lua | 20 +++++++++++++++++++ 5 files changed, 35 insertions(+), 4 deletions(-) diff --git a/worlds/factorio/Mod.py b/worlds/factorio/Mod.py index 46d0a455..6fae00e4 100644 --- a/worlds/factorio/Mod.py +++ b/worlds/factorio/Mod.py @@ -109,7 +109,8 @@ 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: template_data[factorio_option] = getattr(multiworld, factorio_option)[player].value diff --git a/worlds/factorio/Options.py b/worlds/factorio/Options.py index abce73c5..66d7a0be 100644 --- a/worlds/factorio/Options.py +++ b/worlds/factorio/Options.py @@ -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" @@ -306,6 +314,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, diff --git a/worlds/factorio/__init__.py b/worlds/factorio/__init__.py index 54a8e953..ec2a5766 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 @@ -146,7 +146,7 @@ class Factorio(World): else 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 \ + satellite_recipe = None if self.world.goal[self.player].value != Goal.option_rocket \ else 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) 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] From 22d8b0ef300b98ccbc03e50d93cde26fb143b1f6 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Thu, 2 Dec 2021 03:14:26 +0100 Subject: [PATCH 10/65] Clients: add hint_location for autofill --- kvui.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kvui.py b/kvui.py index 73ef26de..9d755ea9 100644 --- a/kvui.py +++ b/kvui.py @@ -207,7 +207,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): From f673dfb7cfa48613ac10823dddab4d03f092b538 Mon Sep 17 00:00:00 2001 From: black-sliver <59490463+black-sliver@users.noreply.github.com> Date: Wed, 1 Dec 2021 01:27:51 +0100 Subject: [PATCH 11/65] SNIClient: add #server= to url for soe/wasm client --- SNIClient.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/SNIClient.py b/SNIClient.py index ca63eb2a..64f562b0 100644 --- a/SNIClient.py +++ b/SNIClient.py @@ -1067,7 +1067,8 @@ async def main(): 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) From 83cfd6ec052f60dbedac86e5ed70d1962a1dc697 Mon Sep 17 00:00:00 2001 From: lordlou <87331798+lordlou@users.noreply.github.com> Date: Thu, 2 Dec 2021 00:11:42 -0500 Subject: [PATCH 12/65] SM update (#147) * fixed generations failing when only bosses are unreachable * - replaced setting maxDiff to infinity with a bool only affecting boss logics if only bosses are left to finish * fixed failling generations when using 'fun' settings Accessibility checks are forced to 'items' if restricted locations are used by VARIA following usage of 'fun' settings * fixed debug logger * removed unsupported "suits_restriction" option * fixed generations failing when only bosses are unreachable (using a less intrusive approach for AP) * - fixed deathlink emptying reserves - added death_link_survive option that lets player survive when receiving a deathlink if the have non-empty reserves * - merged death_link and death_link_survive options --- SNIClient.py | 13 ++++++- worlds/sm/Options.py | 18 ++++++--- worlds/sm/__init__.py | 37 ++++++++++++------- worlds/sm/variaRandomizer/graph/graph.py | 4 +- worlds/sm/variaRandomizer/logic/helpers.py | 10 +++++ .../sm/variaRandomizer/logic/smboolmanager.py | 3 +- .../variaRandomizer/rando/ItemLocContainer.py | 4 +- worlds/sm/variaRandomizer/utils/utils.py | 2 +- 8 files changed, 64 insertions(+), 27 deletions(-) diff --git a/SNIClient.py b/SNIClient.py index 64f562b0..2229eadf 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() diff --git a/worlds/sm/Options.py b/worlds/sm/Options.py index 16ccb5d2..4950b03a 100644 --- a/worlds/sm/Options.py +++ b/worlds/sm/Options.py @@ -40,6 +40,14 @@ class StartLocation(Choice): option_Golden_Four = 14 default = 1 +class DeathLinkSurvive(Choice): + """When DeathLink is enabled and someone dies, you can survive with (enable_survive) if you have non-empty reserve tank.""" + displayname = "Death Link Survive" + option_disable = 0 + option_enable = 1 + option_enable_survive = 3 + default = 0 + class MaxDifficulty(Choice): displayname = "Maximum Difficulty" option_easy = 0 @@ -57,9 +65,6 @@ class MorphPlacement(Choice): option_normal = 1 default = 0 -class SuitsRestriction(DefaultOnToggle): - displayname = "Suits Restriction" - class StrictMinors(Toggle): displayname = "Strict Minors" @@ -117,12 +122,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): @@ -188,7 +196,7 @@ sm_options: typing.Dict[str, type(Option)] = { "start_inventory_removes_from_pool": StartItemsRemovesFromPool, "preset": Preset, "start_location": StartLocation, - "death_link": DeathLink, + "death_link_survive": DeathLinkSurvive, #"majors_split": "Full", #"scav_num_locs": "10", #"scav_randomized": "off", @@ -197,7 +205,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..f60b63d3 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 @@ -274,7 +278,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_survive[self.player].value]} playerNames = {} playerNameIDMap = {} @@ -476,17 +480,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 +487,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/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 From a60c6176be53d5ecc44853dce9c437d8ea1ef3f0 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Thu, 2 Dec 2021 06:13:37 +0100 Subject: [PATCH 13/65] SM: add client version check for DeathLink --- worlds/sm/__init__.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/worlds/sm/__init__.py b/worlds/sm/__init__.py index f60b63d3..6eaa0f6e 100644 --- a/worlds/sm/__init__.py +++ b/worlds/sm/__init__.py @@ -161,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) From a767d7723c00bd31fb0f3b993233f8aa31a7aa20 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Thu, 2 Dec 2021 07:14:55 +0100 Subject: [PATCH 14/65] FF1: update some client texts --- FF1Client.py | 10 +++++----- kvui.py | 7 +++++++ 2 files changed, 12 insertions(+), 5 deletions(-) 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/kvui.py b/kvui.py index 9d755ea9..8039f659 100644 --- a/kvui.py +++ b/kvui.py @@ -365,6 +365,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) From 1ec9ab5568d4d6bdff3fb9a32d36895cb8ebc324 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Thu, 2 Dec 2021 07:47:10 +0100 Subject: [PATCH 15/65] CommonClient: make the Server tooltip no longer fullscreen --- data/client.kv | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/data/client.kv b/data/client.kv index 200ba024..84afd22d 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: 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 From 548d893eaaf1d7b60c45d2fe8d307d4ca54e1239 Mon Sep 17 00:00:00 2001 From: CaitSith2 Date: Wed, 1 Dec 2021 23:42:09 -0800 Subject: [PATCH 16/65] Convenient runtime changing of death link status requires 0.2.1 --- worlds/factorio/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worlds/factorio/__init__.py b/worlds/factorio/__init__.py index ec2a5766..3dc6e7d3 100644 --- a/worlds/factorio/__init__.py +++ b/worlds/factorio/__init__.py @@ -171,7 +171,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 From a15689e380a8cd22bffd4408e0e976b753d153d8 Mon Sep 17 00:00:00 2001 From: CaitSith2 Date: Thu, 2 Dec 2021 09:26:51 -0800 Subject: [PATCH 17/65] Allow explicit blacklisting (and whitelisting) of free samples from yaml --- worlds/factorio/Mod.py | 4 ++++ worlds/factorio/Options.py | 13 +++++++++++++ 2 files changed, 17 insertions(+) diff --git a/worlds/factorio/Mod.py b/worlds/factorio/Mod.py index 6fae00e4..07c8b6ea 100644 --- a/worlds/factorio/Mod.py +++ b/worlds/factorio/Mod.py @@ -113,6 +113,8 @@ def generate_mod(world, output_directory: str): "goal": multiworld.goal[player].value} for factorio_option in Options.factorio_options: + if factorio_option == "free_sample_blacklist": + continue template_data[factorio_option] = getattr(multiworld, factorio_option)[player].value if getattr(multiworld, "silo")[player].value == Options.Silo.option_randomize_recipe: @@ -121,6 +123,8 @@ 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(multiworld.free_sample_blacklist[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 66d7a0be..a13c0e60 100644 --- a/worlds/factorio/Options.py +++ b/worlds/factorio/Options.py @@ -154,6 +154,18 @@ class FactorioStartItems(ItemDict): default = {"burner-mining-drill": 19, "stone-furnace": 19} +class FactorioFreeSampleBlacklist(OptionDict): + """any non-zero value means that item is blacklisted from free samples. zero overrides the built-in blacklist""" + displayname = "Free Sample Blacklist" + + def __init__(self, value: typing.Dict[str, int]): + self.value = value or {} + if any(type(value) not in [int, bool] for value in self.value.values()): + raise Exception("Cannot have non-number blacklist options") + for key in self.value.keys(): + self.value[key] = 1 if self.value[key] else 0 + + class TrapCount(Range): range_end = 4 @@ -322,6 +334,7 @@ factorio_options: typing.Dict[str, type(Option)] = { "free_samples": FreeSamples, "tech_tree_information": TechTreeInformation, "starting_items": FactorioStartItems, + "free_sample_blacklist": FactorioFreeSampleBlacklist, "recipe_time": RecipeTime, "recipe_ingredients": RecipeIngredients, "imported_blueprints": ImportedBlueprint, From efb4e5a7b38da806cba309b8b89298a92eb80e33 Mon Sep 17 00:00:00 2001 From: CaitSith2 Date: Thu, 2 Dec 2021 15:27:00 -0800 Subject: [PATCH 18/65] Use OptionSet for blacklist --- worlds/factorio/Mod.py | 2 +- worlds/factorio/Options.py | 12 ++---------- 2 files changed, 3 insertions(+), 11 deletions(-) diff --git a/worlds/factorio/Mod.py b/worlds/factorio/Mod.py index 07c8b6ea..90580ab4 100644 --- a/worlds/factorio/Mod.py +++ b/worlds/factorio/Mod.py @@ -123,7 +123,7 @@ 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(multiworld.free_sample_blacklist[player].value) + template_data["free_sample_blacklist"].update({item: 1 for item in multiworld.free_sample_blacklist[player].value}) control_code = control_template.render(**template_data) data_template_code = data_template.render(**template_data) diff --git a/worlds/factorio/Options.py b/worlds/factorio/Options.py index a13c0e60..2da6d974 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 @@ -154,17 +154,9 @@ class FactorioStartItems(ItemDict): default = {"burner-mining-drill": 19, "stone-furnace": 19} -class FactorioFreeSampleBlacklist(OptionDict): - """any non-zero value means that item is blacklisted from free samples. zero overrides the built-in blacklist""" +class FactorioFreeSampleBlacklist(OptionSet): displayname = "Free Sample Blacklist" - def __init__(self, value: typing.Dict[str, int]): - self.value = value or {} - if any(type(value) not in [int, bool] for value in self.value.values()): - raise Exception("Cannot have non-number blacklist options") - for key in self.value.keys(): - self.value[key] = 1 if self.value[key] else 0 - class TrapCount(Range): range_end = 4 From 6f12ed38d95d0b53a0af2d386d2e9f0b9dcfc74b Mon Sep 17 00:00:00 2001 From: CaitSith2 Date: Thu, 2 Dec 2021 15:27:48 -0800 Subject: [PATCH 19/65] Add in whitelist for overriding blacklist. --- worlds/factorio/Mod.py | 3 ++- worlds/factorio/Options.py | 6 ++++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/worlds/factorio/Mod.py b/worlds/factorio/Mod.py index 90580ab4..0b10e8f5 100644 --- a/worlds/factorio/Mod.py +++ b/worlds/factorio/Mod.py @@ -113,7 +113,7 @@ def generate_mod(world, output_directory: str): "goal": multiworld.goal[player].value} for factorio_option in Options.factorio_options: - if factorio_option == "free_sample_blacklist": + if factorio_option in ["free_sample_blacklist", "free_sample_whitelist"]: continue template_data[factorio_option] = getattr(multiworld, factorio_option)[player].value @@ -124,6 +124,7 @@ def generate_mod(world, output_directory: str): 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) diff --git a/worlds/factorio/Options.py b/worlds/factorio/Options.py index 2da6d974..38991102 100644 --- a/worlds/factorio/Options.py +++ b/worlds/factorio/Options.py @@ -158,6 +158,11 @@ 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 @@ -327,6 +332,7 @@ factorio_options: typing.Dict[str, type(Option)] = { "tech_tree_information": TechTreeInformation, "starting_items": FactorioStartItems, "free_sample_blacklist": FactorioFreeSampleBlacklist, + "free_sample_whitelist": FactorioFreeSampleWhitelist, "recipe_time": RecipeTime, "recipe_ingredients": RecipeIngredients, "imported_blueprints": ImportedBlueprint, From 3110763052cd7362a0ec2a32d6e361fc18fe8f99 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Fri, 3 Dec 2021 02:41:56 +0100 Subject: [PATCH 20/65] WebHost: allow switching out "/tracker/" for "/generic_tracker/" in a tracker url to get the generic tracker for that slot. No idea where a good place is to sick a link for it. Maybe on the individual trackers pages? --- WebHostLib/tracker.py | 9 +++++++-- data/client.kv | 2 +- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/WebHostLib/tracker.py b/WebHostLib/tracker.py index 59102707..c91c2e8a 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: diff --git a/data/client.kv b/data/client.kv index 84afd22d..09be0188 100644 --- a/data/client.kv +++ b/data/client.kv @@ -33,7 +33,7 @@ : size: self.texture_size size_hint: None, None - font_size: 18 + font_size: dp(18) pos_hint: {'center_y': 0.5, 'center_x': 0.5} halign: "left" canvas.before: From 9d3cbb19f9eb6cc6a9b34511136c6dde9d8e6cba Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Fri, 3 Dec 2021 05:14:44 +0100 Subject: [PATCH 21/65] Clients: add docstrings to /items and /locations --- CommonClient.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CommonClient.py b/CommonClient.py index ea8170b1..cb7b94c9 100644 --- a/CommonClient.py +++ b/CommonClient.py @@ -82,11 +82,13 @@ 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) From 994621372cbe92b4f836392a3f267a48c365a852 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Fri, 3 Dec 2021 05:24:43 +0100 Subject: [PATCH 22/65] MultiServer: finish removing prompt toolkit --- MultiServer.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) 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 From a3220ac72d86cb326e0319e1bc7ceeb59f14a08a Mon Sep 17 00:00:00 2001 From: eudaimonistic <94811100+eudaimonistic@users.noreply.github.com> Date: Sun, 21 Nov 2021 21:00:33 -0500 Subject: [PATCH 23/65] Add known safe MSU-1 list List assembled for use in competitive Zelda restreams. Permission sought and granted by author Amarith via DM. --- WebHostLib/static/assets/tutorial/zelda3/msu1_en.md | 1 + 1 file changed, 1 insertion(+) 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) From e7d8149d74d0e55535cbe0f92bf1f73f658ba968 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Fri, 3 Dec 2021 07:01:21 +0100 Subject: [PATCH 24/65] LttP Docs: reword instructions to not accidentally overwrite the SNI Connector with an empty file. --- WebHostLib/customserver.py | 8 +++++--- .../static/assets/tutorial/zelda3/multiworld_en.md | 9 +++++---- 2 files changed, 10 insertions(+), 7 deletions(-) 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/static/assets/tutorial/zelda3/multiworld_en.md b/WebHostLib/static/assets/tutorial/zelda3/multiworld_en.md index 737de947..5fc07cc1 100644 --- a/WebHostLib/static/assets/tutorial/zelda3/multiworld_en.md +++ b/WebHostLib/static/assets/tutorial/zelda3/multiworld_en.md @@ -76,7 +76,7 @@ Firewall. 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 @@ -85,10 +85,11 @@ Firewall. 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 Scipt... +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 From 47c1300f309bb7baa6feb755d42839dd0933c8d8 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Fri, 3 Dec 2021 07:01:43 +0100 Subject: [PATCH 25/65] Setup: move templates from /Players into /Players/Templates --- setup.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py index 0911ce5e..76d953eb 100644 --- a/setup.py +++ b/setup.py @@ -146,7 +146,7 @@ extra_data = ["LICENSE", "data", "EnemizerCLI", "host.yaml", "SNI", "meta.yaml"] 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,7 @@ 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) try: from maseya import z3pr From 21d465bcb8bebb28dceb54adf1b807995aabb08b Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Fri, 3 Dec 2021 07:04:17 +0100 Subject: [PATCH 26/65] CommonClient: add docstring to /ready --- CommonClient.py | 1 + WebHostLib/static/assets/tutorial/zelda3/multiworld_en.md | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/CommonClient.py b/CommonClient.py index cb7b94c9..6f9d28d7 100644 --- a/CommonClient.py +++ b/CommonClient.py @@ -94,6 +94,7 @@ class ClientCommandProcessor(CommandProcessor): 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/WebHostLib/static/assets/tutorial/zelda3/multiworld_en.md b/WebHostLib/static/assets/tutorial/zelda3/multiworld_en.md index 5fc07cc1..f75d3b21 100644 --- a/WebHostLib/static/assets/tutorial/zelda3/multiworld_en.md +++ b/WebHostLib/static/assets/tutorial/zelda3/multiworld_en.md @@ -85,11 +85,11 @@ Firewall. 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 Script -> Open Scipt... +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/lua` -6. Run the script by double clicking it in the listing +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 From c10e17d24c027d7486593949efda615451fa979b Mon Sep 17 00:00:00 2001 From: espeon65536 Date: Wed, 1 Dec 2021 15:42:15 -0600 Subject: [PATCH 27/65] Minecraft: remove bad default for StartingItems --- worlds/minecraft/Options.py | 1 - 1 file changed, 1 deletion(-) diff --git a/worlds/minecraft/Options.py b/worlds/minecraft/Options.py index 04e87c06..edf6a93f 100644 --- a/worlds/minecraft/Options.py +++ b/worlds/minecraft/Options.py @@ -92,7 +92,6 @@ class SendDefeatedMobs(Toggle): class StartingItems(OptionList): """Start with these items. Each entry should be of this format: {item: "item_name", amount: #, nbt: "nbt_string"}""" displayname = "Starting Items" - default = 0 minecraft_options: typing.Dict[str, type(Option)] = { From 75625b143c1286d3ef83700af22553adaf7cf7c9 Mon Sep 17 00:00:00 2001 From: espeon65536 Date: Wed, 1 Dec 2021 21:53:52 -0600 Subject: [PATCH 28/65] Core: better pretty-print for OptionList when the list is of non-strings --- Options.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Options.py b/Options.py index 03fb0c84..cd4d9148 100644 --- a/Options.py +++ b/Options.py @@ -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 From c1a73e7839a58b13d681f92d9515c866031184eb Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Fri, 3 Dec 2021 20:44:26 +0100 Subject: [PATCH 29/65] WebHost: document how to bring up a slot tracker --- WebHostLib/templates/tracker.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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() %} From 84e76eadd90a1aa7942c52c16f8c8744b3fe16ac Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Fri, 3 Dec 2021 22:11:25 +0100 Subject: [PATCH 30/65] SM: rename death_link_survive and update docstring --- worlds/sm/Options.py | 8 ++++---- worlds/sm/__init__.py | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/worlds/sm/Options.py b/worlds/sm/Options.py index 4950b03a..25d53c49 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,8 +40,8 @@ class StartLocation(Choice): option_Golden_Four = 14 default = 1 -class DeathLinkSurvive(Choice): - """When DeathLink is enabled and someone dies, you can survive with (enable_survive) if you have non-empty reserve tank.""" +class DeathLink(Choice): + """When DeathLink is enabled and someone dies, you will die. With survive reserve tanks can save you.""" displayname = "Death Link Survive" option_disable = 0 option_enable = 1 @@ -196,7 +196,7 @@ sm_options: typing.Dict[str, type(Option)] = { "start_inventory_removes_from_pool": StartItemsRemovesFromPool, "preset": Preset, "start_location": StartLocation, - "death_link_survive": DeathLinkSurvive, + "death_link": DeathLink, #"majors_split": "Full", #"scav_num_locs": "10", #"scav_randomized": "off", diff --git a/worlds/sm/__init__.py b/worlds/sm/__init__.py index 6eaa0f6e..6abcbfbe 100644 --- a/worlds/sm/__init__.py +++ b/worlds/sm/__init__.py @@ -282,7 +282,7 @@ class SMWorld(World): openTourianGreyDoors = {0x07C823 + 5: [0x0C], 0x07C831 + 5: [0x0C]} - deathLink = {0x277f04: [self.world.death_link_survive[self.player].value]} + deathLink = {0x277f04: [self.world.death_link[self.player].value]} playerNames = {} playerNameIDMap = {} From feb2e0be0396b23102ef93ee070025d3d35bdebe Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Sat, 4 Dec 2021 10:54:11 +0100 Subject: [PATCH 31/65] Factorio: fix selecting wrong goal requirements due to convoluted if tree. --- worlds/factorio/__init__.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/worlds/factorio/__init__.py b/worlds/factorio/__init__.py index 3dc6e7d3..6166e73c 100644 --- a/worlds/factorio/__init__.py +++ b/worlds/factorio/__init__.py @@ -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.goal[self.player].value != Goal.option_rocket \ - 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) From 9c74d648f8dda1fd4336a5343227efe6cc3311f4 Mon Sep 17 00:00:00 2001 From: CaitSith2 Date: Sat, 4 Dec 2021 06:20:16 -0800 Subject: [PATCH 32/65] Tie the need for satellite recipe to satellite goal, not max science pack. --- worlds/factorio/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worlds/factorio/__init__.py b/worlds/factorio/__init__.py index 6166e73c..0a12b7f2 100644 --- a/worlds/factorio/__init__.py +++ b/worlds/factorio/__init__.py @@ -341,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: From 33477202b9da09c3edd63bae3dcb61e1cfa037c3 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Sat, 4 Dec 2021 22:12:09 +0100 Subject: [PATCH 33/65] WebHost: remove outdated data --- .../static/static/weightedSettings.json | 2146 ----------------- .../static/static/weightedSettings.yaml | 449 ---- 2 files changed, 2595 deletions(-) delete mode 100644 WebHostLib/static/static/weightedSettings.json delete mode 100644 WebHostLib/static/static/weightedSettings.yaml 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 From 98a038e39e2186acb2b942dcbbc5dee5f388c1b8 Mon Sep 17 00:00:00 2001 From: CaitSith2 Date: Sat, 4 Dec 2021 14:04:28 -0800 Subject: [PATCH 34/65] Death link default true/false values for super metroid. --- worlds/sm/Options.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/worlds/sm/Options.py b/worlds/sm/Options.py index 25d53c49..b9f9ba64 100644 --- a/worlds/sm/Options.py +++ b/worlds/sm/Options.py @@ -46,6 +46,8 @@ class DeathLink(Choice): option_disable = 0 option_enable = 1 option_enable_survive = 3 + alias_false = 0 + alias_true = 1 default = 0 class MaxDifficulty(Choice): From ba9974fe2acd6d1fe4655a09bf2375768d3870b6 Mon Sep 17 00:00:00 2001 From: Hussein Farran Date: Thu, 2 Dec 2021 23:04:35 -0500 Subject: [PATCH 35/65] Update README.md --- README.md | 1 + 1 file changed, 1 insertion(+) 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 From 85efee143245a3ce33c09f484b108a6fcfbd3470 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Wed, 8 Dec 2021 09:27:58 +0100 Subject: [PATCH 36/65] SM: raise Exception instead of sys.exit for custom presets --- worlds/sm/variaRandomizer/randomizer.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/worlds/sm/variaRandomizer/randomizer.py b/worlds/sm/variaRandomizer/randomizer.py index d87c3e39..5b1312fe 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) From 6cd08ea8dc97f0370afab9840888d173f33a5d5a Mon Sep 17 00:00:00 2001 From: jtoyoda Date: Sat, 4 Dec 2021 16:49:42 -0700 Subject: [PATCH 37/65] Updating ff1 gameinfo --- WebHostLib/static/assets/gameInfo/en_Final Fantasy.md | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/WebHostLib/static/assets/gameInfo/en_Final Fantasy.md b/WebHostLib/static/assets/gameInfo/en_Final Fantasy.md index 40628ce1..b625404b 100644 --- a/WebHostLib/static/assets/gameInfo/en_Final Fantasy.md +++ b/WebHostLib/static/assets/gameInfo/en_Final Fantasy.md @@ -12,12 +12,8 @@ locations. So ,for example, Princess Sarah may have the CANOE instead of the LUT Pot or some armor. There are plenty of other things that can be randomized on our [main randomizer site](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 From 21fbb545e8f2b9bb95a05591829a2ba196d3d9ab Mon Sep 17 00:00:00 2001 From: jtoyoda Date: Sat, 4 Dec 2021 16:50:40 -0700 Subject: [PATCH 38/65] Adding in missing comas in ff1 game info --- WebHostLib/static/assets/gameInfo/en_Final Fantasy.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/WebHostLib/static/assets/gameInfo/en_Final Fantasy.md b/WebHostLib/static/assets/gameInfo/en_Final Fantasy.md index b625404b..cf139178 100644 --- a/WebHostLib/static/assets/gameInfo/en_Final Fantasy.md +++ b/WebHostLib/static/assets/gameInfo/en_Final Fantasy.md @@ -13,7 +13,8 @@ Pot or some armor. There are plenty of other things that can be randomized on ou [main randomizer site](https://finalfantasyrandomizer.com/) ## What Final Fantasy items can appear in other players' worlds? -All items can appear in other players worlds. This includes consumables, shards, weapons, armor and of course key items. +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 From e1fc44f4e01c8bb327d2cb9ae5a8be9cff344a9e Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Fri, 10 Dec 2021 09:29:59 +0100 Subject: [PATCH 39/65] Clients: compatibility change for old Intel graphics. --- SNIClient.py | 1 + kvui.py | 9 ++++++--- setup.py | 3 ++- worlds/sm/variaRandomizer/randomizer.py | 4 +--- 4 files changed, 10 insertions(+), 7 deletions(-) diff --git a/SNIClient.py b/SNIClient.py index 2229eadf..6d7540ea 100644 --- a/SNIClient.py +++ b/SNIClient.py @@ -1048,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: diff --git a/kvui.py b/kvui.py index 8039f659..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 @@ -431,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 76d953eb..c72846b1 100644 --- a/setup.py +++ b/setup.py @@ -141,7 +141,7 @@ 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)) @@ -155,6 +155,7 @@ for worldname, worldtype in AutoWorldRegister.world_types.items(): file_name = worldname+".yaml" shutil.copyfile(os.path.join("WebHostLib", "static", "generated", "configs", file_name), buildfolder / "Players" / "Templates" / file_name) +shutil.copyfile("meta.yaml", buildfolder / "Players" / "Templates" / "meta.yaml") try: from maseya import z3pr diff --git a/worlds/sm/variaRandomizer/randomizer.py b/worlds/sm/variaRandomizer/randomizer.py index 5b1312fe..a95764df 100644 --- a/worlds/sm/variaRandomizer/randomizer.py +++ b/worlds/sm/variaRandomizer/randomizer.py @@ -364,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': From 7afbf8b45bd74bb9cea0d550d942dd4a82de66dc Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Fri, 10 Dec 2021 09:53:50 +0100 Subject: [PATCH 40/65] OoTAdjuster: check on subprocess compressor --- worlds/oot/Rom.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) 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))) From 4fe024041df48b4b14edac68585feb6eb7ac6c07 Mon Sep 17 00:00:00 2001 From: espeon65536 Date: Fri, 10 Dec 2021 08:33:52 -0600 Subject: [PATCH 41/65] Minecraft client: update Forge to 1.17.1-37.1.1 This fixes the critical security issue recently found in Minecraft. --- MinecraftClient.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MinecraftClient.py b/MinecraftClient.py index e178c00b..fdd88d39 100644 --- a/MinecraftClient.py +++ b/MinecraftClient.py @@ -15,7 +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.0.109" +forge_version = "1.17.1-37.1.1" def prompt_yes_no(prompt): From 706fc19ab4214ad64a47622243d18669a87e6e75 Mon Sep 17 00:00:00 2001 From: alwaysintreble Date: Fri, 10 Dec 2021 17:43:28 -0600 Subject: [PATCH 42/65] tutorials: place a missing / oops --- .../static/assets/tutorial/archipelago/advanced_settings_en.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/WebHostLib/static/assets/tutorial/archipelago/advanced_settings_en.md b/WebHostLib/static/assets/tutorial/archipelago/advanced_settings_en.md index 0432efb8..e007ce9e 100644 --- a/WebHostLib/static/assets/tutorial/archipelago/advanced_settings_en.md +++ b/WebHostLib/static/assets/tutorial/archipelago/advanced_settings_en.md @@ -76,7 +76,7 @@ that can be rolled by these settings. If a game can be rolled it **must** have a Some options in Archipelago can be used by every game but must still be placed within the relevant game's section. Currently, these options are `start_inventory`, `start_hints`, `local_items`, `non_local_items`, `start_location_hints`, -`exclude_locations`, and various [plando options](tutorial/archipelago/plando/en). +`exclude_locations`, and various [plando options](/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 30 rupees. From 3bf367d630220314d7c309c082ce7b9af68ea450 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Mon, 13 Dec 2021 01:38:07 +0100 Subject: [PATCH 43/65] WebHost: don't bother queuing empty commands --- WebHostLib/__init__.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/WebHostLib/__init__.py b/WebHostLib/__init__.py index d32267d2..e4ada5bc 100644 --- a/WebHostLib/__init__.py +++ b/WebHostLib/__init__.py @@ -166,8 +166,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 From 3f20bdaaa2bbd79c61bee82785333d8aa2924274 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Mon, 13 Dec 2021 05:48:33 +0100 Subject: [PATCH 44/65] WebHost: split autolaunch and autogen services --- WebHost.py | 4 +++- WebHostLib/__init__.py | 5 +++-- WebHostLib/autolauncher.py | 31 +++++++++++++++++++++++-------- 3 files changed, 29 insertions(+), 11 deletions(-) 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 e4ada5bc..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 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 = {} From 0558351a12af3f8607deb8a76734d16b13c6b418 Mon Sep 17 00:00:00 2001 From: CaitSith2 Date: Mon, 13 Dec 2021 20:24:54 +0100 Subject: [PATCH 45/65] Allow update_sprites to work on strict text only systems --- LttPAdjuster.py | 48 ++++++++++++++++++++++++++++++++------- WebHostLib/lttpsprites.py | 13 +++++++---- 2 files changed, 49 insertions(+), 12 deletions(-) diff --git a/LttPAdjuster.py b/LttPAdjuster.py index 77732059..f66cd038 100644 --- a/LttPAdjuster.py +++ b/LttPAdjuster.py @@ -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: @@ -241,12 +242,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): @@ -400,12 +405,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/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 = [] From f003c7130f93bb0940e9b3b302f791328ec0d9b7 Mon Sep 17 00:00:00 2001 From: TauAkiou Date: Tue, 14 Dec 2021 11:04:24 -0500 Subject: [PATCH 46/65] [WebHost] Add Super Metroid support to Web Tracker (#153) * [WebHost]: Added Super Metroid tracker, based on TimeSpinner & OOT --- .../static/assets/supermetroidTracker.js | 49 +++++++++ .../static/styles/supermetroidTracker.css | 104 ++++++++++++++++++ WebHostLib/templates/supermetroidTracker.html | 85 ++++++++++++++ WebHostLib/tracker.py | 103 ++++++++++++++++- 4 files changed, 340 insertions(+), 1 deletion(-) create mode 100644 WebHostLib/static/assets/supermetroidTracker.js create mode 100644 WebHostLib/static/styles/supermetroidTracker.css create mode 100644 WebHostLib/templates/supermetroidTracker.html 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/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/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/tracker.py b/WebHostLib/tracker.py index c91c2e8a..228bf375 100644 --- a/WebHostLib/tracker.py +++ b/WebHostLib/tracker.py @@ -774,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, @@ -887,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 From 5a2e477dba6062cf21bb4e9daa11e56edca89138 Mon Sep 17 00:00:00 2001 From: Jarno Westhof Date: Sat, 11 Dec 2021 14:16:55 +0100 Subject: [PATCH 47/65] Added sanity check to see if all locations can be assigned to regions --- worlds/timespinner/Regions.py | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/worlds/timespinner/Regions.py b/worlds/timespinner/Regions.py index 5a3423fc..237bbc3d 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,10 @@ def create_regions(world: MultiWorld, player: int, locations: Tuple[LocationData create_region(world, player, locations_per_region, location_cache, 'Space time continuum') ] + throwIfAnyLocationIsNotAssignedToARegion(regions, locations_per_region.keys()) + + world.regions += regions + connectStartingRegion(world, player) names: Dict[str, int] = {} @@ -150,6 +154,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 From 13036539b7b0b477c8d3db62b3ed2c67f35ae659 Mon Sep 17 00:00:00 2001 From: Jarno Westhof Date: Sat, 11 Dec 2021 21:05:45 +0100 Subject: [PATCH 48/65] TS: Starting with Jewelrybox, Talaria or Meyef in your starting inventory will now set the corresponding flag --- worlds/timespinner/__init__.py | 43 ++++++++++++++++++++++------------ 1 file changed, 28 insertions(+), 15 deletions(-) diff --git a/worlds/timespinner/__init__.py b/worlds/timespinner/__init__.py index b0602f18..b4d914bb 100644 --- a/worlds/timespinner/__init__.py +++ b/worlds/timespinner/__init__.py @@ -24,19 +24,32 @@ 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) + + # for item in self.world.precollected_items[self.player]: + # if item.name in self.remove_from_start_inventory: 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) + 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,7 +57,7 @@ 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) @@ -52,14 +65,14 @@ class TimespinnerWorld(World): def generate_basic(self): excluded_items = get_excluded_items_based_on_options(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,15 +86,15 @@ 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]: excluded_items: Set[str] = set() From c0b83843192827bcfc3867be06eca078ff3289ab Mon Sep 17 00:00:00 2001 From: Jarno Westhof Date: Sun, 12 Dec 2021 14:52:22 +0100 Subject: [PATCH 49/65] TS: putting non consumable items in starting inventory will now remove them from the pool so a duplicate wont drop --- worlds/timespinner/__init__.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/worlds/timespinner/__init__.py b/worlds/timespinner/__init__.py index b4d914bb..e813c5cb 100644 --- a/worlds/timespinner/__init__.py +++ b/worlds/timespinner/__init__.py @@ -35,8 +35,9 @@ class TimespinnerWorld(World): self.location_cache = [] self.pyramid_keys_unlock = get_pyramid_keys_unlock(world, player) - # for item in self.world.precollected_items[self.player]: - # if item.name in self.remove_from_start_inventory: +#TODO +#Non local items not getting rewarded locally +#Do not reward starting progression item if you already got one as your starting item def generate_early(self): if self.world.start_inventory[self.player].value.pop('Meyef', 0) > 0: @@ -63,7 +64,7 @@ class TimespinnerWorld(World): def generate_basic(self): - excluded_items = get_excluded_items_based_on_options(self.world, self.player) + excluded_items = get_excluded_items_based(self, self.world, self.player) assign_starter_items(self.world, self.player, excluded_items, self.locked_locations) @@ -96,7 +97,7 @@ class TimespinnerWorld(World): 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_based(self: TimespinnerWorld, world: MultiWorld, player: int) -> Set[str]: excluded_items: Set[str] = set() if is_option_enabled(world, player, "StartWithJewelryBox"): @@ -105,6 +106,10 @@ 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 From db456cbcf180571224489dde4b747a5ce1c5cc81 Mon Sep 17 00:00:00 2001 From: Jarno Westhof Date: Sun, 12 Dec 2021 17:59:08 +0100 Subject: [PATCH 50/65] TS: no longer reward a progression item if you already have one in your starting inventory --- worlds/timespinner/__init__.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/worlds/timespinner/__init__.py b/worlds/timespinner/__init__.py index e813c5cb..a70ddee2 100644 --- a/worlds/timespinner/__init__.py +++ b/worlds/timespinner/__init__.py @@ -37,9 +37,9 @@ class TimespinnerWorld(World): #TODO #Non local items not getting rewarded locally -#Do not reward starting progression item if you already got one as your starting item def generate_early(self): + # in generate_early the start_inventory isnt copied over to precollected_items, so we can still moffify 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: @@ -64,7 +64,7 @@ class TimespinnerWorld(World): def generate_basic(self): - excluded_items = get_excluded_items_based(self, 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) @@ -97,7 +97,7 @@ class TimespinnerWorld(World): spoiler_handle.write('Twin Pyramid Keys unlock: %s\n' % (self.pyramid_keys_unlock)) -def get_excluded_items_based(self: TimespinnerWorld, 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"): @@ -151,6 +151,10 @@ 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]): + for item in world.precollected_items[player]: + if item.name in starter_progression_items: + return + progression_item = world.random.choice(starter_progression_items) location = world.random.choice(starter_progression_locations) From 3f36c436adfd55754ee5ceea97589afaa622eac7 Mon Sep 17 00:00:00 2001 From: Jarno Westhof Date: Sun, 12 Dec 2021 23:43:30 +0100 Subject: [PATCH 51/65] TS: putting items as non local will correctly be handled by your starting orbs and your first progression item excluding locations now correctly works for your first progression item in an non inverted seed Aura blast can now be your starting spell --- worlds/timespinner/Items.py | 1 + worlds/timespinner/__init__.py | 52 +++++++++++++++++++++++++--------- 2 files changed, 40 insertions(+), 13 deletions(-) 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/__init__.py b/worlds/timespinner/__init__.py index a70ddee2..1791c360 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 @@ -115,20 +115,38 @@ def get_excluded_items(self: TimespinnerWorld, world: MultiWorld, player: int) - 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]: @@ -155,8 +173,16 @@ def place_first_progression_item(world: MultiWorld, player: int, excluded_items: if item.name in starter_progression_items: return - progression_item = world.random.choice(starter_progression_items) - location = world.random.choice(starter_progression_locations) + 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) From c4981e4b91ddca3b043c20b6dd900899f7ad787d Mon Sep 17 00:00:00 2001 From: Jarno Westhof Date: Sun, 12 Dec 2021 23:53:49 +0100 Subject: [PATCH 52/65] TS: Fixed unit test --- worlds/timespinner/PyramidKeys.py | 3 +++ 1 file changed, 3 insertions(+) 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 From 5d0748983b1f8cffcffc54a0f7b28e86f09a4ab8 Mon Sep 17 00:00:00 2001 From: Jarno Westhof Date: Sun, 12 Dec 2021 23:54:43 +0100 Subject: [PATCH 53/65] TS: removed todo list :D --- worlds/timespinner/__init__.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/worlds/timespinner/__init__.py b/worlds/timespinner/__init__.py index 1791c360..a720aa91 100644 --- a/worlds/timespinner/__init__.py +++ b/worlds/timespinner/__init__.py @@ -35,8 +35,6 @@ class TimespinnerWorld(World): self.location_cache = [] self.pyramid_keys_unlock = get_pyramid_keys_unlock(world, player) -#TODO -#Non local items not getting rewarded locally def generate_early(self): # in generate_early the start_inventory isnt copied over to precollected_items, so we can still moffify the options directly From 9e4cb6ee33954f01394bfc8baa0c8be3eb539892 Mon Sep 17 00:00:00 2001 From: Jarno Westhof Date: Tue, 14 Dec 2021 14:04:34 +0100 Subject: [PATCH 54/65] TS: Fixed review comments --- worlds/timespinner/Regions.py | 3 ++- worlds/timespinner/__init__.py | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/worlds/timespinner/Regions.py b/worlds/timespinner/Regions.py index 237bbc3d..91b072ec 100644 --- a/worlds/timespinner/Regions.py +++ b/worlds/timespinner/Regions.py @@ -45,7 +45,8 @@ def create_regions(world: MultiWorld, player: int, locations: Tuple[LocationData create_region(world, player, locations_per_region, location_cache, 'Space time continuum') ] - throwIfAnyLocationIsNotAssignedToARegion(regions, locations_per_region.keys()) + if __debug__: + throwIfAnyLocationIsNotAssignedToARegion(regions, locations_per_region.keys()) world.regions += regions diff --git a/worlds/timespinner/__init__.py b/worlds/timespinner/__init__.py index a720aa91..623de7c6 100644 --- a/worlds/timespinner/__init__.py +++ b/worlds/timespinner/__init__.py @@ -37,7 +37,7 @@ class TimespinnerWorld(World): def generate_early(self): - # in generate_early the start_inventory isnt copied over to precollected_items, so we can still moffify the options directly + # 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: From af96f71190acb569332c2836b298089828443f49 Mon Sep 17 00:00:00 2001 From: CaitSith2 Date: Thu, 16 Dec 2021 15:34:18 -0800 Subject: [PATCH 55/65] Fix bug where there is less locations than hint count. --- worlds/alttp/Rom.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 From aa40e811f1735cdb524c4fa4e86a16a72cd83fc3 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Fri, 17 Dec 2021 19:17:41 +0100 Subject: [PATCH 56/65] LttPAdjuster: ignore alttpr cert --- LttPAdjuster.py | 10 ++++------ worlds/sm/Options.py | 2 +- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/LttPAdjuster.py b/LttPAdjuster.py index f66cd038..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): @@ -119,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) @@ -225,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) @@ -259,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: @@ -267,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) @@ -294,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): diff --git a/worlds/sm/Options.py b/worlds/sm/Options.py index b9f9ba64..010e8017 100644 --- a/worlds/sm/Options.py +++ b/worlds/sm/Options.py @@ -42,7 +42,7 @@ class StartLocation(Choice): class DeathLink(Choice): """When DeathLink is enabled and someone dies, you will die. With survive reserve tanks can save you.""" - displayname = "Death Link Survive" + displayname = "Death Link" option_disable = 0 option_enable = 1 option_enable_survive = 3 From 450e0eacf4567ce46c802c74667f742d0dc8b01d Mon Sep 17 00:00:00 2001 From: Jarno Westhof Date: Fri, 17 Dec 2021 14:10:40 +0100 Subject: [PATCH 57/65] TS: Relaxed entry logic for lower caves --- worlds/timespinner/Regions.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/worlds/timespinner/Regions.py b/worlds/timespinner/Regions.py index 91b072ec..9db742eb 100644 --- a/worlds/timespinner/Regions.py +++ b/worlds/timespinner/Regions.py @@ -99,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")) @@ -119,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') From c42f53d64f734b53d84d9642d030cd82c21938b2 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Sat, 18 Dec 2021 13:01:24 +0100 Subject: [PATCH 58/65] Factorio: add some more tech tree shapes --- worlds/factorio/Options.py | 2 ++ worlds/factorio/Shapes.py | 50 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 52 insertions(+) diff --git a/worlds/factorio/Options.py b/worlds/factorio/Options.py index 38991102..b8b06cb2 100644 --- a/worlds/factorio/Options.py +++ b/worlds/factorio/Options.py @@ -94,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 diff --git a/worlds/factorio/Shapes.py b/worlds/factorio/Shapes.py index 12e9f0b6..4a622de9 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 @@ -189,6 +190,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 From 3a2a584ad382866f47e2b97a09f68796ef882797 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Sat, 18 Dec 2021 13:05:43 +0100 Subject: [PATCH 59/65] Factorio: fix singles layout not generating correctly. --- worlds/factorio/Shapes.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/worlds/factorio/Shapes.py b/worlds/factorio/Shapes.py index 4a622de9..eaf44ac1 100644 --- a/worlds/factorio/Shapes.py +++ b/worlds/factorio/Shapes.py @@ -22,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] From 5fa1185d6d63554f0843a875c9d91589e6dcaf91 Mon Sep 17 00:00:00 2001 From: black-sliver <59490463+black-sliver@users.noreply.github.com> Date: Sat, 18 Dec 2021 16:00:54 +0100 Subject: [PATCH 60/65] SoE: make doc point to upstream guide.md --- WebHostLib/static/assets/gameInfo/en_Secret of Evermore.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/WebHostLib/static/assets/gameInfo/en_Secret of Evermore.md b/WebHostLib/static/assets/gameInfo/en_Secret of Evermore.md index 255cbc72..b8e30adf 100644 --- a/WebHostLib/static/assets/gameInfo/en_Secret of Evermore.md +++ b/WebHostLib/static/assets/gameInfo/en_Secret of Evermore.md @@ -10,7 +10,7 @@ so the game is always able to be completed. However, because of the item shuffle 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 [guide](https://github.com/black-sliver/evermizer/blob/feat-mw/guide.md). +Additional help can be found in the [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 From 70aae514bec31b50b6dad952dc7d533fc345a5d9 Mon Sep 17 00:00:00 2001 From: black-sliver <59490463+black-sliver@users.noreply.github.com> Date: Sat, 18 Dec 2021 16:14:16 +0100 Subject: [PATCH 61/65] SoE: fix macos wheel urls --- worlds/soe/requirements.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/worlds/soe/requirements.txt b/worlds/soe/requirements.txt index 814d6f14..4350b723 100644 --- a/worlds/soe/requirements.txt +++ b/worlds/soe/requirements.txt @@ -7,8 +7,8 @@ https://github.com/black-sliver/pyevermizer/releases/download/v0.39.2/pyevermize 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-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.39.2/pyevermizer-0.39.2-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.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' bsdiff4>=1.2.1 From 1603bab1dae0648547201c3e1ecb93e53257e618 Mon Sep 17 00:00:00 2001 From: black-sliver <59490463+black-sliver@users.noreply.github.com> Date: Sat, 18 Dec 2021 16:39:47 +0100 Subject: [PATCH 62/65] SoE: Rename difficulty 'Chaos' to 'Mystery' --- worlds/soe/Options.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/worlds/soe/Options.py b/worlds/soe/Options.py index 9eddd0e7..10406c01 100644 --- a/worlds/soe/Options.py +++ b/worlds/soe/Options.py @@ -30,7 +30,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'] From 7f03a86dee0a8fda849a22e00301e4d854e4df33 Mon Sep 17 00:00:00 2001 From: black-sliver <59490463+black-sliver@users.noreply.github.com> Date: Sun, 19 Dec 2021 05:34:41 +0100 Subject: [PATCH 63/65] SoE: Rename 'chaos' to 'full' in options * was changed upstream * also update tooltips to be a bit more helpful --- worlds/soe/Options.py | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/worlds/soe/Options.py b/worlds/soe/Options.py index 10406c01..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 @@ -89,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'] @@ -121,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 From 9172cc49258097975b69905abca3476989f79615 Mon Sep 17 00:00:00 2001 From: black-sliver <59490463+black-sliver@users.noreply.github.com> Date: Sun, 19 Dec 2021 05:42:48 +0100 Subject: [PATCH 64/65] SoE: Update to pyevermizer v0.40.0 see https://github.com/black-sliver/pyevermizer/releases/tag/v0.40.0 --- worlds/soe/requirements.txt | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/worlds/soe/requirements.txt b/worlds/soe/requirements.txt index 4350b723..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_x86_64.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_x86_64.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 From 3ee4be2e33dd8c8a2ca6df53394c1d00e86880b7 Mon Sep 17 00:00:00 2001 From: espeon65536 Date: Sun, 19 Dec 2021 13:27:40 -0500 Subject: [PATCH 65/65] Minecraft client: more general search for mod name --- MinecraftClient.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/MinecraftClient.py b/MinecraftClient.py index fdd88d39..cc4e0820 100644 --- a/MinecraftClient.py +++ b/MinecraftClient.py @@ -35,12 +35,10 @@ def prompt_yes_no(prompt): 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)