From e1ee08a599442b668f8b14b81e71eefdf67afe9f Mon Sep 17 00:00:00 2001 From: Aaron Wagener Date: Tue, 10 Oct 2023 18:51:13 -0500 Subject: [PATCH 01/54] FFR: create items in create_items (#2291) --- worlds/ff1/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worlds/ff1/__init__.py b/worlds/ff1/__init__.py index 56b41d62d0..432467399e 100644 --- a/worlds/ff1/__init__.py +++ b/worlds/ff1/__init__.py @@ -91,7 +91,7 @@ class FF1World(World): def set_rules(self): self.multiworld.completion_condition[self.player] = lambda state: state.has(CHAOS_TERMINATED_EVENT, self.player) - def generate_basic(self): + def create_items(self): items = get_options(self.multiworld, 'items', self.player) if FF1_BRIDGE in items.keys(): self._place_locked_item_in_sphere0(FF1_BRIDGE) From 1ef3bc78dc0875d8e2973b8660b531c39404af14 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Wed, 11 Oct 2023 19:21:02 +0200 Subject: [PATCH 02/54] CommonClient: inherit Context tags (#2283) --- CommonClient.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CommonClient.py b/CommonClient.py index 154b61b1d5..a5e9b4553a 100644 --- a/CommonClient.py +++ b/CommonClient.py @@ -882,7 +882,7 @@ def get_base_parser(description: typing.Optional[str] = None): def run_as_textclient(): class TextContext(CommonContext): # Text Mode to use !hint and such with games that have no text entry - tags = {"AP", "TextOnly"} + tags = CommonContext.tags | {"TextOnly"} game = "" # empty matches any game since 0.3.2 items_handling = 0b111 # receive all items for /received want_slot_data = False # Can't use game specific slot_data From 19d649f92b8d012575936afe4758396999458259 Mon Sep 17 00:00:00 2001 From: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com> Date: Fri, 13 Oct 2023 01:46:16 +0200 Subject: [PATCH 03/54] The Witness: Update docs (outdated information) (#2294) * Update Witness Game Page * Update outdated Witness Setup Guide * Incorporate suggestions --- worlds/witness/docs/en_The Witness.md | 7 ++++--- worlds/witness/docs/setup_en.md | 8 +++++--- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/worlds/witness/docs/en_The Witness.md b/worlds/witness/docs/en_The Witness.md index 2ae478bed0..4d00ecaae4 100644 --- a/worlds/witness/docs/en_The Witness.md +++ b/worlds/witness/docs/en_The Witness.md @@ -10,12 +10,13 @@ config file. Puzzles are randomly generated using the popular [Sigma Rando](https://github.com/sigma144/witness-randomizer). They are made to be similar to the original game, but with different solutions. -Ontop of that each puzzle symbol (Squares, Stars, Dots, etc.) is now an item. +On top of that, each puzzle symbol (Squares, Stars, Dots, etc.) is now an item. Panels with puzzle symbols on them are now locked initially. ## What is a "check" in The Witness? Solving the last panel in a row of panels or an important standalone panel will count as a check, and send out an item. +It is also possible to add Environmental Puzzles into the location pool via the "Shuffle Environmental Puzzles" setting. ## What "items" can you unlock in The Witness? @@ -32,7 +33,7 @@ By default, the audio logs scattered around the world will have 10 hints for you Example: "Shipwreck Vault contains Triangles". -## The Jungle, Orchard, Forest and Color House aren't randomized. What gives? +## The Jungle, Orchard, Forest and Color Bunker aren't randomized. What gives? There are limitations to what can currently be randomized in The Witness. There is an option to turn these non-randomized panels off, called "disable_non_randomized" in your yaml file. This will also slightly change the activation requirement of certain panels, detailed [here](https://github.com/sigma144/witness-randomizer/wiki/Activation-Triggers). @@ -46,4 +47,4 @@ In this case, the generator will make its best attempt to adjust logic according One of the use cases of this could be to pre-open a specific door or pre-activate a single laser. In "shuffle_EPs: obelisk_sides", any Environmental Puzzles in exclude_locations will be pre-completed and not considered for their Obelisk Side. -If every Environmental Puzzle on an Obelisk Side is pre-completed, that side disappears from the location pool entirely. \ No newline at end of file +If every Environmental Puzzle on an Obelisk Side is pre-completed, that side disappears from the location pool entirely. diff --git a/worlds/witness/docs/setup_en.md b/worlds/witness/docs/setup_en.md index 94a50846f9..daa9b8b9b5 100644 --- a/worlds/witness/docs/setup_en.md +++ b/worlds/witness/docs/setup_en.md @@ -29,8 +29,10 @@ To continue an earlier game: ## Archipelago Text Client -It is recommended to have Archipelago's Text Client open on the side to keep track of what items you receive and send, as The Witness has no in-game messages. -
Or use the Auto-Tracker! +It is recommended to have Archipelago's Text Client open on the side to keep track of what items you receive and send. +
The Witness does display received and sent items in-game, but only for a short time, and the messages are easy to miss while playing. + +

Of course, you can also use the Auto-Tracker! ## Auto-Tracking @@ -41,4 +43,4 @@ The Witness has a fully functional map tracker that supports auto-tracking. 3. Click on the "AP" symbol at the top. 4. Enter the AP address, slot name and password. -The rest should take care of itself! Items and checks will be marked automatically, and it even knows your settings - It will hide checks & adjust logic accordingly. Note that the tracker may be out of date. \ No newline at end of file +The rest should take care of itself! Items and checks will be marked automatically, and it even knows your settings - It will hide checks & adjust logic accordingly. From 8fc304269e90c1cc2fbce2d9d0806ddb51f657a4 Mon Sep 17 00:00:00 2001 From: Shiny <36184001+ShinyNT@users.noreply.github.com> Date: Thu, 12 Oct 2023 20:51:10 -0300 Subject: [PATCH 04/54] Docs: add Spanish guide for Muse Dash (#2297) * adding setup_es * Update setup_es.md * Update setup_es.md * Update __init__.py referencing setup_es on init.py * Update __init__.py fixing a space --- worlds/musedash/__init__.py | 11 +++++++- worlds/musedash/docs/setup_es.md | 48 ++++++++++++++++++++++++++++++++ 2 files changed, 58 insertions(+), 1 deletion(-) create mode 100644 worlds/musedash/docs/setup_es.md diff --git a/worlds/musedash/__init__.py b/worlds/musedash/__init__.py index 78b9c253d5..754d2352e0 100644 --- a/worlds/musedash/__init__.py +++ b/worlds/musedash/__init__.py @@ -23,7 +23,16 @@ class MuseDashWebWorld(WebWorld): ["DeamonHunter"] ) - tutorials = [setup_en] + setup_es = Tutorial( + setup_en.tutorial_name, + setup_en.description, + "Español", + "setup_es.md", + "setup/es", + ["Shiny"] + ) + + tutorials = [setup_en, setup_es] class MuseDashWorld(World): diff --git a/worlds/musedash/docs/setup_es.md b/worlds/musedash/docs/setup_es.md new file mode 100644 index 0000000000..21fc69e7eb --- /dev/null +++ b/worlds/musedash/docs/setup_es.md @@ -0,0 +1,48 @@ +# Guía de instalación para Muse Dash: Archipelago + +## Enlaces rápidos +- [Página Principal](../../../../games/Muse%20Dash/info/en) +- [Página de Configuraciones](../../../../games/Muse%20Dash/player-settings) + +## Software Requerido + +- Windows 8 o más reciente. +- Muse Dash: [Disponible en Steam](https://store.steampowered.com/app/774171/Muse_Dash/) + - \[Opcional\] [Just as Planned] DLC: [tambien disponible on Steam](https://store.steampowered.com/app/1055810/Muse_Dash__Just_as_planned/) +- Melon Loader: [GitHub](https://github.com/LavaGang/MelonLoader/releases/latest) + - .Net Framework 4.8 podría ser necesario para el instalador: [Descarga](https://dotnet.microsoft.com/en-us/download/dotnet-framework/net48) +- .Net 6.0 (si aún no está instalado): [Descarga](https://dotnet.microsoft.com/en-us/download/dotnet/6.0#runtime-6.0.15) +- Muse Dash Archipelago Mod: [GitHub](https://github.com/DeamonHunter/ArchipelagoMuseDash/releases/latest) + +## Instalar el mod de Archipelago en Muse Dash + +1. Descarga [MelonLoader.Installer.exe](https://github.com/LavaGang/MelonLoader/releases/latest) y ejecutalo. +2. Elije la pestaña "automated", haz clic en el botón "select" y busca tu `MuseDash.exe`. Luego haz clic en "install". + - Puedes encontrar la carpeta en Steam buscando el juego en tu biblioteca, haciendo clic derecho sobre el y elegir *Administrar→Ver archivos locales*. + - Si haces clic en la barra superior que te indica la carpeta en la que estas, te dará la dirección de ésta para que puedas copiarla. Al pegar esa dirección en la ventana que **MelonLoader** abre, irá automaticamente a esa carpeta. +3. Ejecuta el juego una vez, y espera hasta que aparezca la pantalla de inicio de Muse Dash antes de cerrarlo. +4. Descarga la última version de [Muse Dash Archipelago Mod](https://github.com/DeamonHunter/ArchipelagoMuseDash/releases/latest) y extraelo en la nueva carpeta creada llamada `/Mods/`, localizada en la carpeta de instalación de Muse Dash. + - Todos los archivos deben ir directamente en la carpeta `/Mods/`, y NO en una subcarpeta dentro de la carpeta `/Mods/` + +Si todo fue instalado correctamente, un botón aparecerá en la parte inferior derecha del juego una vez abierto, que te permitirá conectarte al servidor de Archipelago. + +## Generar un juego MultiWorld +1. Entra a la página de [configuraciones de jugador](/games/Muse%20Dash/player-settings) y configura las opciones del juego a tu gusto. +2. Genera tu archivo YAML y úsalo para generar un juego nuevo en el radomizer + - (Instrucciones sobre como generar un juego en Archipelago disponibles en la [guía web de Archipelago en Inglés](/tutorial/Archipelago/setup/en)) + +## Unirse a un juego MultiWorld + +1. Ejecuta Muse Dash y pasa por la pantalla de introducción. Haz clic en el botón de la esquina inferior derecha. +2. Ingresa los detalles de la sesión de archipelago, como la dirección del servidor con el puerto (por ejemplo, archipelago.gg:38381), nombre de usuario y contraseña. +3. Si todo se ingresó correctamente, el pop-up debería desaparecer y se mostrará el menú principal habitual. Al ingresar a la selección de canciones, deberías ver una cantidad limitada de canciones. + +## Solución de problemas + +### No Support Module Loaded + +Este error ocurre cuando Melon Loader no puede encontrar los archivos necesarios para ejecutar mods. Generalmente, hay dos razones principales de este error: una falla al generar los archivos cuando el juego se ejecutó por primera vez con Melon Loader, o un antivirus que elimina los archivos después de la generación. + +Para solucionar este problema, primero debes eliminar Melon Loader de Muse Dash. Puedes hacer esto eliminando la carpeta Melon Loader dentro de la carpeta de Muse Dash. Luego, seguir los pasos de instalación nuevamente. + +Si continúas teniendo problemas y estás utilizando un antivirus, es posible que tengas que desactivarlo temporalmente cuando se ejecute Muse Dash por primera vez, o excluir la carpeta Muse Dash de ser escaneada. From fffbe68428e6ac6b562af9f5e2be7e653b1eea44 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Sun, 15 Oct 2023 04:51:52 +0200 Subject: [PATCH 05/54] Subnautica: cleanup pass (#2293) --- worlds/subnautica/__init__.py | 60 +++++++++---------- .../subnautica/{Creatures.py => creatures.py} | 0 worlds/subnautica/{Exports.py => exports.py} | 4 +- worlds/subnautica/{Items.py => items.py} | 0 .../subnautica/{Locations.py => locations.py} | 0 worlds/subnautica/{Options.py => options.py} | 4 +- worlds/subnautica/{Rules.py => rules.py} | 31 +++++----- worlds/subnautica/test/__init__.py | 12 ++-- 8 files changed, 55 insertions(+), 56 deletions(-) rename worlds/subnautica/{Creatures.py => creatures.py} (100%) rename worlds/subnautica/{Exports.py => exports.py} (95%) rename worlds/subnautica/{Items.py => items.py} (100%) rename worlds/subnautica/{Locations.py => locations.py} (100%) rename worlds/subnautica/{Options.py => options.py} (98%) rename worlds/subnautica/{Rules.py => rules.py} (92%) diff --git a/worlds/subnautica/__init__.py b/worlds/subnautica/__init__.py index 2d4cf2faf6..7b25b61c81 100644 --- a/worlds/subnautica/__init__.py +++ b/worlds/subnautica/__init__.py @@ -6,12 +6,12 @@ from typing import List, Dict, Any, cast from BaseClasses import Region, Entrance, Location, Item, Tutorial, ItemClassification from worlds.AutoWorld import World, WebWorld -from . import Items -from . import Locations -from . import Creatures -from . import Options -from .Items import item_table, group_items, items_by_type, ItemType -from .Rules import set_rules +from . import items +from . import locations +from . import creatures +from . import options +from .items import item_table, group_items, items_by_type, ItemType +from .rules import set_rules logger = logging.getLogger("Subnautica") @@ -27,8 +27,8 @@ class SubnaticaWeb(WebWorld): )] -all_locations = {data["name"]: loc_id for loc_id, data in Locations.location_table.items()} -all_locations.update(Creatures.creature_locations) +all_locations = {data["name"]: loc_id for loc_id, data in locations.location_table.items()} +all_locations.update(creatures.creature_locations) class SubnauticaWorld(World): @@ -40,9 +40,9 @@ class SubnauticaWorld(World): game = "Subnautica" web = SubnaticaWeb() - item_name_to_id = {data.name: item_id for item_id, data in Items.item_table.items()} + item_name_to_id = {data.name: item_id for item_id, data in items.item_table.items()} location_name_to_id = all_locations - option_definitions = Options.options + option_definitions = options.option_definitions data_version = 10 required_client_version = (0, 4, 1) @@ -50,37 +50,37 @@ class SubnauticaWorld(World): creatures_to_scan: List[str] def generate_early(self) -> None: - if self.multiworld.early_seaglide[self.player]: + if self.options.early_seaglide: self.multiworld.local_early_items[self.player]["Seaglide Fragment"] = 2 - scan_option: Options.AggressiveScanLogic = self.multiworld.creature_scan_logic[self.player] + scan_option: options.AggressiveScanLogic = self.options.creature_scan_logic creature_pool = scan_option.get_pool() - self.multiworld.creature_scans[self.player].value = min( + self.options.creature_scans.value = min( len(creature_pool), - self.multiworld.creature_scans[self.player].value + self.options.creature_scans.value ) - self.creatures_to_scan = self.multiworld.random.sample( - creature_pool, self.multiworld.creature_scans[self.player].value) + self.creatures_to_scan = self.random.sample( + creature_pool, self.options.creature_scans.value) def create_regions(self): self.multiworld.regions += [ self.create_region("Menu", None, ["Lifepod 5"]), self.create_region("Planet 4546B", - Locations.events + - [location["name"] for location in Locations.location_table.values()] + - [creature+Creatures.suffix for creature in self.creatures_to_scan]) + locations.events + + [location["name"] for location in locations.location_table.values()] + + [creature + creatures.suffix for creature in self.creatures_to_scan]) ] # Link regions self.multiworld.get_entrance("Lifepod 5", self.player).connect(self.multiworld.get_region("Planet 4546B", self.player)) - for event in Locations.events: + for event in locations.events: self.multiworld.get_location(event, self.player).place_locked_item( SubnauticaItem(event, ItemClassification.progression, None, player=self.player)) # make the goal event the victory "item" - self.multiworld.get_location(self.multiworld.goal[self.player].get_event_name(), self.player).item.name = "Victory" + self.multiworld.get_location(self.options.goal.get_event_name(), self.player).item.name = "Victory" # refer to Rules.py set_rules = set_rules @@ -88,7 +88,7 @@ class SubnauticaWorld(World): def create_items(self): # Generate item pool pool: List[SubnauticaItem] = [] - extras = self.multiworld.creature_scans[self.player].value + extras = self.options.creature_scans.value grouped = set(itertools.chain.from_iterable(group_items.values())) @@ -139,17 +139,15 @@ class SubnauticaWorld(World): self.multiworld.itempool += pool def fill_slot_data(self) -> Dict[str, Any]: - goal: Options.Goal = self.multiworld.goal[self.player] - swim_rule: Options.SwimRule = self.multiworld.swim_rule[self.player] vanilla_tech: List[str] = [] slot_data: Dict[str, Any] = { - "goal": goal.current_key, - "swim_rule": swim_rule.current_key, + "goal": self.options.goal.current_key, + "swim_rule": self.options.swim_rule.current_key, "vanilla_tech": vanilla_tech, "creatures_to_scan": self.creatures_to_scan, - "death_link": self.multiworld.death_link[self.player].value, - "free_samples": self.multiworld.free_samples[self.player].value, + "death_link": self.options.death_link.value, + "free_samples": self.options.free_samples.value, } return slot_data @@ -161,10 +159,10 @@ class SubnauticaWorld(World): item_table[item_id].classification, item_id, player=self.player) - def create_region(self, name: str, locations=None, exits=None): + def create_region(self, name: str, region_locations=None, exits=None): ret = Region(name, self.player, self.multiworld) - if locations: - for location in locations: + if region_locations: + for location in region_locations: loc_id = self.location_name_to_id.get(location, None) location = SubnauticaLocation(self.player, location, loc_id, ret) ret.locations.append(location) diff --git a/worlds/subnautica/Creatures.py b/worlds/subnautica/creatures.py similarity index 100% rename from worlds/subnautica/Creatures.py rename to worlds/subnautica/creatures.py diff --git a/worlds/subnautica/Exports.py b/worlds/subnautica/exports.py similarity index 95% rename from worlds/subnautica/Exports.py rename to worlds/subnautica/exports.py index 7a69cffc8d..911c6a6f69 100644 --- a/worlds/subnautica/Exports.py +++ b/worlds/subnautica/exports.py @@ -12,8 +12,8 @@ if __name__ == "__main__": os.chdir(new_home) sys.path.append(new_home) - from worlds.subnautica.Locations import Vector, location_table - from worlds.subnautica.Items import item_table, group_items, items_by_type + from worlds.subnautica.locations import Vector, location_table + from worlds.subnautica.items import item_table, group_items, items_by_type from NetUtils import encode export_folder = os.path.join(new_home, "Subnautica Export") diff --git a/worlds/subnautica/Items.py b/worlds/subnautica/items.py similarity index 100% rename from worlds/subnautica/Items.py rename to worlds/subnautica/items.py diff --git a/worlds/subnautica/Locations.py b/worlds/subnautica/locations.py similarity index 100% rename from worlds/subnautica/Locations.py rename to worlds/subnautica/locations.py diff --git a/worlds/subnautica/Options.py b/worlds/subnautica/options.py similarity index 98% rename from worlds/subnautica/Options.py rename to worlds/subnautica/options.py index 582e93eb0e..d8d727a9e1 100644 --- a/worlds/subnautica/Options.py +++ b/worlds/subnautica/options.py @@ -1,7 +1,7 @@ import typing from Options import Choice, Range, DeathLink, Toggle, DefaultOnToggle, StartInventoryPool -from .Creatures import all_creatures, Definitions +from .creatures import all_creatures, Definitions class SwimRule(Choice): @@ -103,7 +103,7 @@ class SubnauticaDeathLink(DeathLink): Note: can be toggled via in-game console command "deathlink".""" -options = { +option_definitions = { "swim_rule": SwimRule, "early_seaglide": EarlySeaglide, "free_samples": FreeSamples, diff --git a/worlds/subnautica/Rules.py b/worlds/subnautica/rules.py similarity index 92% rename from worlds/subnautica/Rules.py rename to worlds/subnautica/rules.py index 793c85be41..3b6c5cd4dd 100644 --- a/worlds/subnautica/Rules.py +++ b/worlds/subnautica/rules.py @@ -1,9 +1,9 @@ from typing import TYPE_CHECKING, Dict, Callable, Optional from worlds.generic.Rules import set_rule, add_rule -from .Locations import location_table, LocationDict -from .Creatures import all_creatures, aggressive, suffix, hatchable, containment -from .Options import AggressiveScanLogic, SwimRule +from .locations import location_table, LocationDict +from .creatures import all_creatures, aggressive, suffix, hatchable, containment +from .options import AggressiveScanLogic, SwimRule import math if TYPE_CHECKING: @@ -290,16 +290,16 @@ aggression_rules: Dict[int, Callable[["CollectionState", int], bool]] = { def set_rules(subnautica_world: "SubnauticaWorld"): player = subnautica_world.player - world = subnautica_world.multiworld + multiworld = subnautica_world.multiworld for loc in location_table.values(): - set_location_rule(world, player, loc) + set_location_rule(multiworld, player, loc) if subnautica_world.creatures_to_scan: - option = world.creature_scan_logic[player] + option = multiworld.creature_scan_logic[player] for creature_name in subnautica_world.creatures_to_scan: - location = set_creature_rule(world, player, creature_name) + location = set_creature_rule(multiworld, player, creature_name) if creature_name in containment: # there is no other way, hard-required containment add_rule(location, lambda state: has_containment(state, player)) elif creature_name in aggressive: @@ -309,7 +309,7 @@ def set_rules(subnautica_world: "SubnauticaWorld"): lambda state, loc_rule=get_aggression_rule(option, creature_name): loc_rule(state, player)) # Victory locations - set_rule(world.get_location("Neptune Launch", player), + set_rule(multiworld.get_location("Neptune Launch", player), lambda state: get_max_depth(state, player) >= 1444 and has_mobile_vehicle_bay(state, player) and @@ -322,13 +322,14 @@ def set_rules(subnautica_world: "SubnauticaWorld"): state.has("Ion Battery", player) and has_cyclops_shield(state, player)) - set_rule(world.get_location("Disable Quarantine", player), lambda state: - get_max_depth(state, player) >= 1444) + set_rule(multiworld.get_location("Disable Quarantine", player), + lambda state: get_max_depth(state, player) >= 1444) - set_rule(world.get_location("Full Infection", player), lambda state: - get_max_depth(state, player) >= 900) + set_rule(multiworld.get_location("Full Infection", player), + lambda state: get_max_depth(state, player) >= 900) - room = world.get_location("Aurora Drive Room - Upgrade Console", player) - set_rule(world.get_location("Repair Aurora Drive", player), lambda state: room.can_reach(state)) + room = multiworld.get_location("Aurora Drive Room - Upgrade Console", player) + set_rule(multiworld.get_location("Repair Aurora Drive", player), + lambda state: room.can_reach(state)) - world.completion_condition[player] = lambda state: state.has("Victory", player) + multiworld.completion_condition[player] = lambda state: state.has("Victory", player) diff --git a/worlds/subnautica/test/__init__.py b/worlds/subnautica/test/__init__.py index d938830737..69f0b6c7cd 100644 --- a/worlds/subnautica/test/__init__.py +++ b/worlds/subnautica/test/__init__.py @@ -15,11 +15,11 @@ class SubnauticaTest(unittest.TestCase): self.assertGreater(self.scancutoff, id) def testGroupAssociation(self): - from worlds.subnautica import Items - for item_id, item_data in Items.item_table.items(): - if item_data.type == Items.ItemType.group: + from worlds.subnautica import items + for item_id, item_data in items.item_table.items(): + if item_data.type == items.ItemType.group: with self.subTest(item=item_data.name): - self.assertIn(item_id, Items.group_items) - for item_id in Items.group_items: + self.assertIn(item_id, items.group_items) + for item_id in items.group_items: with self.subTest(item_id=item_id): - self.assertEqual(Items.item_table[item_id].type, Items.ItemType.group) + self.assertEqual(items.item_table[item_id].type, items.ItemType.group) From 63c7f1deae36884e44ed586cc919d987448c084e Mon Sep 17 00:00:00 2001 From: el-u <109771707+el-u@users.noreply.github.com> Date: Sun, 15 Oct 2023 04:53:28 +0200 Subject: [PATCH 06/54] lufia2ac: switch to new options system (#2289) --- worlds/lufia2ac/Options.py | 5 +++-- worlds/lufia2ac/__init__.py | 9 +++++---- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/worlds/lufia2ac/Options.py b/worlds/lufia2ac/Options.py index c3bbadc9ba..3f1c58f9d0 100644 --- a/worlds/lufia2ac/Options.py +++ b/worlds/lufia2ac/Options.py @@ -5,7 +5,8 @@ from dataclasses import dataclass from itertools import accumulate, chain, combinations from typing import Any, cast, Dict, Iterator, List, Mapping, Optional, Set, Tuple, Type, TYPE_CHECKING, Union -from Options import AssembleOptions, Choice, DeathLink, ItemDict, Range, SpecialRange, TextChoice, Toggle +from Options import AssembleOptions, Choice, DeathLink, ItemDict, PerGameCommonOptions, Range, SpecialRange, \ + TextChoice, Toggle from .Enemies import enemy_name_to_sprite if TYPE_CHECKING: @@ -697,7 +698,7 @@ class ShufflePartyMembers(Toggle): @dataclass -class L2ACOptions: +class L2ACOptions(PerGameCommonOptions): blue_chest_chance: BlueChestChance blue_chest_count: BlueChestCount boss: Boss diff --git a/worlds/lufia2ac/__init__.py b/worlds/lufia2ac/__init__.py index f14048a2c4..fad7109a4a 100644 --- a/worlds/lufia2ac/__init__.py +++ b/worlds/lufia2ac/__init__.py @@ -2,11 +2,11 @@ import base64 import itertools import os from enum import IntFlag -from typing import Any, ClassVar, Dict, get_type_hints, Iterator, List, Set, Tuple +from typing import Any, ClassVar, Dict, Iterator, List, Set, Tuple, Type import settings from BaseClasses import Item, ItemClassification, Location, MultiWorld, Region, Tutorial -from Options import AssembleOptions +from Options import PerGameCommonOptions from Utils import __version__ from worlds.AutoWorld import WebWorld, World from worlds.generic.Rules import add_rule, set_rule @@ -54,7 +54,8 @@ class L2ACWorld(World): game: ClassVar[str] = "Lufia II Ancient Cave" web: ClassVar[WebWorld] = L2ACWeb() - option_definitions: ClassVar[Dict[str, AssembleOptions]] = get_type_hints(L2ACOptions) + options_dataclass: ClassVar[Type[PerGameCommonOptions]] = L2ACOptions + options: L2ACOptions settings: ClassVar[L2ACSettings] item_name_to_id: ClassVar[Dict[str, int]] = l2ac_item_name_to_id location_name_to_id: ClassVar[Dict[str, int]] = l2ac_location_name_to_id @@ -87,7 +88,7 @@ class L2ACWorld(World): bytearray(f"L2AC{__version__.replace('.', '')[:3]}_{self.player}_{self.multiworld.seed}", "utf8")[:21] self.rom_name.extend([0] * (21 - len(self.rom_name))) - self.o = L2ACOptions(**{opt: getattr(self.multiworld, opt)[self.player] for opt in self.option_definitions}) + self.o = self.options if self.o.blue_chest_count < self.o.custom_item_pool.count: raise ValueError(f"Number of items in custom_item_pool ({self.o.custom_item_pool.count}) is " From e27aeac2e5f9a1f70baeabe6ef0d0508db6d31a8 Mon Sep 17 00:00:00 2001 From: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> Date: Mon, 16 Oct 2023 20:59:07 -0400 Subject: [PATCH 07/54] HK: Update Setup Guide to use/mention Lumafly (#2308) --- worlds/hk/docs/setup_en.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/worlds/hk/docs/setup_en.md b/worlds/hk/docs/setup_en.md index adf975ff51..fef0f051fe 100644 --- a/worlds/hk/docs/setup_en.md +++ b/worlds/hk/docs/setup_en.md @@ -1,27 +1,27 @@ # Hollow Knight for Archipelago Setup Guide ## Required Software -* Download and unzip the Scarab+ Mod Manager from the [Scarab+ website](https://themulhima.github.io/Scarab/). +* Download and unzip the Lumafly Mod Manager from the [Lumafly website](https://themulhima.github.io/Lumafly/). * A legal copy of Hollow Knight. -## Installing the Archipelago Mod using Scarab+ -1. Launch Scarab+ and ensure it locates your Hollow Knight installation directory. +## Installing the Archipelago Mod using Lumafly +1. Launch Lumafly and ensure it locates your Hollow Knight installation directory. 2. Click the "Install" button near the "Archipelago" mod entry. * If desired, also install "Archipelago Map Mod" to use as an in-game tracker. 3. Launch the game, you're all set! -### What to do if Scarab+ fails to find your XBox Game Pass installation directory +### What to do if Lumafly fails to find your XBox Game Pass installation directory 1. Enter the XBox app and move your mouse over "Hollow Knight" on the left sidebar. 2. Click the three points then click "Manage". 3. Go to the "Files" tab and select "Browse...". 4. Click "Hollow Knight", then "Content", then click the path bar and copy it. -5. Run Scarab+ as an administrator and, when it asks you for the path, paste what you copied in step 4. +5. Run Lumafly as an administrator and, when it asks you for the path, paste what you copied in step 4. #### Alternative Method: 1. Click on your profile then "Settings". 2. Go to the "General" tab and select "CHANGE FOLDER". 3. Look for a folder where you want to install the game (preferably inside a folder on your desktop) and copy the path. -4. Run Scarab+ as an administrator and, when it asks you for the path, paste what you copied in step 3. +4. Run Lumafly as an administrator and, when it asks you for the path, paste what you copied in step 3. Note: The path folder needs to have the "Hollow Knight_Data" folder inside. From 13b68ecb154fb09e3cf41e3c3b81a3a1d1423d92 Mon Sep 17 00:00:00 2001 From: Alchav <59858495+Alchav@users.noreply.github.com> Date: Tue, 17 Oct 2023 01:20:34 -0400 Subject: [PATCH 08/54] =?UTF-8?q?Pok=C3=A9mon=20R/B:=20Door=20Shuffle=20fi?= =?UTF-8?q?xes=20(#2314)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Door shuffle fixes * Add Rt 23's Victory Road exit door to list of unreachable outdoor entrances --- worlds/pokemon_rb/locations.py | 2 +- worlds/pokemon_rb/regions.py | 27 ++++++++++++++++----------- 2 files changed, 17 insertions(+), 12 deletions(-) diff --git a/worlds/pokemon_rb/locations.py b/worlds/pokemon_rb/locations.py index ec6375859b..4f1b55a00d 100644 --- a/worlds/pokemon_rb/locations.py +++ b/worlds/pokemon_rb/locations.py @@ -795,7 +795,7 @@ location_data = [ LocationData("Pewter Gym", "Defeat Brock", "Defeat Brock", event=True), LocationData("Cerulean Gym", "Defeat Misty", "Defeat Misty", event=True), LocationData("Vermilion Gym", "Defeat Lt. Surge", "Defeat Lt. Surge", event=True), - LocationData("Celadon Gym", "Defeat Erika", "Defeat Erika", event=True), + LocationData("Celadon Gym-C", "Defeat Erika", "Defeat Erika", event=True), LocationData("Fuchsia Gym", "Defeat Koga", "Defeat Koga", event=True), LocationData("Cinnabar Gym", "Defeat Blaine", "Defeat Blaine", event=True), LocationData("Saffron Gym-C", "Defeat Sabrina", "Defeat Sabrina", event=True), diff --git a/worlds/pokemon_rb/regions.py b/worlds/pokemon_rb/regions.py index cc788dd2ba..431b23f49a 100644 --- a/worlds/pokemon_rb/regions.py +++ b/worlds/pokemon_rb/regions.py @@ -1456,7 +1456,9 @@ mansion_stair_destinations = [ unreachable_outdoor_entrances = [ "Route 4-C to Mt Moon B1F-NE", "Fuchsia City-Good Rod House Backyard to Fuchsia Good Rod House", - "Cerulean City-Badge House Backyard to Cerulean Badge House" + "Cerulean City-Badge House Backyard to Cerulean Badge House", + # TODO: This doesn't need to be forced if fly location is Pokemon League? + "Route 23-N to Victory Road 2F-E" ] @@ -2220,7 +2222,7 @@ def create_regions(self): "Cinnabar Gym - Blaine Prize", "Viridian Gym - Giovanni Prize"]: badge_locs.append(multiworld.get_location(loc, player)) multiworld.random.shuffle(badges) - while badges[3].name == "Cascade Badge" and multiworld.badges_needed_for_hm_moves[player] == "on": + while badges[3].name == "Cascade Badge" and multiworld.badges_needed_for_hm_moves[player]: multiworld.random.shuffle(badges) for badge, loc in zip(badges, badge_locs): loc.place_locked_item(badge) @@ -2266,10 +2268,10 @@ def create_regions(self): ] def adds_reachable_entrances(entrances_copy, item): - state.collect(item, False) + state_copy = state.copy() + state_copy.collect(item, False) ret = len([entrance for entrance in entrances_copy if entrance in reachable_entrances or - entrance.parent_region.can_reach(state)]) > len(reachable_entrances) - state.remove(item) + entrance.parent_region.can_reach(state_copy)]) > len(reachable_entrances) return ret def dead_end(entrances_copy, e): @@ -2304,9 +2306,16 @@ def create_regions(self): starting_entrances = len(entrances) dc_connected = [] event_locations = self.multiworld.get_filled_locations(player) + rock_tunnel_entrances = [entrance for entrance in entrances if "Rock Tunnel" in entrance.name] + entrances = [entrance for entrance in entrances if entrance not in rock_tunnel_entrances] while entrances: state.update_reachable_regions(player) state.sweep_for_events(locations=event_locations) + + if rock_tunnel_entrances and logic.rock_tunnel(state, player): + entrances += rock_tunnel_entrances + rock_tunnel_entrances = None + reachable_entrances = [entrance for entrance in entrances if entrance in reachable_entrances or entrance.parent_region.can_reach(state)] assert reachable_entrances, \ @@ -2328,12 +2337,8 @@ def create_regions(self): # entrances list is empty while it's being sorted, must pass a copy to iterate through entrances_copy = entrances.copy() if multiworld.door_shuffle[player] == "decoupled": - if len(reachable_entrances) <= 8 and not logic.rock_tunnel(state, player): - entrances.sort(key=lambda e: 1 if "Rock Tunnel" in e.name else 2 if e.connected_region is not - None else 3 if e not in reachable_entrances else 0) - else: - entrances.sort(key=lambda e: 1 if e.connected_region is not None else 2 if e not in - reachable_entrances else 0) + entrances.sort(key=lambda e: 1 if e.connected_region is not None else 2 if e not in + reachable_entrances else 0) assert entrances[0].connected_region is None,\ "Ran out of valid reachable entrances in Pokemon Red and Blue door shuffle" elif len(reachable_entrances) > (1 if multiworld.door_shuffle[player] == "insanity" else 8) and len( From 11ebc523a9e96838206bde95e2b15abb3192d4e2 Mon Sep 17 00:00:00 2001 From: Trevor L <80716066+TRPG0@users.noreply.github.com> Date: Wed, 18 Oct 2023 13:50:57 -0600 Subject: [PATCH 09/54] Hylics 2: Various fixes and APWorld support (#2324) - Fix generation failing with certain gesture shuffle options - Fixed passing ItemDict to multidata instead of item code - Don't allow CHARGE UP to be placed at Foglast: TV - APWorld support by removing LogicMixin from Rules.py --- setup.py | 1 - worlds/hylics2/Rules.py | 550 +++++++++++++++++++++++-------------- worlds/hylics2/__init__.py | 12 +- 3 files changed, 349 insertions(+), 214 deletions(-) diff --git a/setup.py b/setup.py index 6d4d947dbd..cea60dab83 100644 --- a/setup.py +++ b/setup.py @@ -71,7 +71,6 @@ non_apworlds: set = { "Clique", "DLCQuest", "Final Fantasy", - "Hylics 2", "Kingdom Hearts 2", "Lufia II Ancient Cave", "Meritous", diff --git a/worlds/hylics2/Rules.py b/worlds/hylics2/Rules.py index 12c22e01cd..6c55c8745b 100644 --- a/worlds/hylics2/Rules.py +++ b/worlds/hylics2/Rules.py @@ -1,91 +1,128 @@ from worlds.generic.Rules import add_rule -from ..AutoWorld import LogicMixin +from BaseClasses import CollectionState -class Hylics2Logic(LogicMixin): +def air_dash(state: CollectionState, player: int) -> bool: + return state.has("PNEUMATOPHORE", player) - def _hylics2_can_air_dash(self, player): - return self.has("PNEUMATOPHORE", player) - def _hylics2_has_airship(self, player): - return self.has("DOCK KEY", player) +def airship(state: CollectionState, player: int) -> bool: + return state.has("DOCK KEY", player) - def _hylics2_has_jail_key(self, player): - return self.has("JAIL KEY", player) - def _hylics2_has_paddle(self, player): - return self.has("PADDLE", player) +def jail_key(state: CollectionState, player: int) -> bool: + return state.has("JAIL KEY", player) - def _hylics2_has_worm_room_key(self, player): - return self.has("WORM ROOM KEY", player) - def _hylics2_has_bridge_key(self, player): - return self.has("BRIDGE KEY", player) +def paddle(state: CollectionState, player: int) -> bool: + return state.has("PADDLE", player) - def _hylics2_has_upper_chamber_key(self, player): - return self.has("UPPER CHAMBER KEY", player) - def _hylics2_has_vessel_room_key(self, player): - return self.has("VESSEL ROOM KEY", player) +def worm_room_key(state: CollectionState, player: int) -> bool: + return state.has("WORM ROOM KEY", player) - def _hylics2_has_house_key(self, player): - return self.has("HOUSE KEY", player) - def _hylics2_has_cave_key(self, player): - return self.has("CAVE KEY", player) +def bridge_key(state: CollectionState, player: int) -> bool: + return state.has("BRIDGE KEY", player) - def _hylics2_has_skull_bomb(self, player): - return self.has("SKULL BOMB", player) - def _hylics2_has_tower_key(self, player): - return self.has("TOWER KEY", player) +def upper_chamber_key(state: CollectionState, player: int) -> bool: + return state.has("UPPER CHAMBER KEY", player) - def _hylics2_has_deep_key(self, player): - return self.has("DEEP KEY", player) - def _hylics2_has_upper_house_key(self, player): - return self.has("UPPER HOUSE KEY", player) +def vessel_room_key(state: CollectionState, player: int) -> bool: + return state.has("VESSEL ROOM KEY", player) - def _hylics2_has_clicker(self, player): - return self.has("CLICKER", player) - def _hylics2_has_tokens(self, player): - return self.has("SAGE TOKEN", player, 3) +def house_key(state: CollectionState, player: int) -> bool: + return state.has("HOUSE KEY", player) - def _hylics2_has_charge_up(self, player): - return self.has("CHARGE UP", player) - def _hylics2_has_cup(self, player): - return self.has("PAPER CUP", player, 1) +def cave_key(state: CollectionState, player: int) -> bool: + return state.has("CAVE KEY", player) - def _hylics2_has_1_member(self, player): - return self.has("Pongorma", player) or self.has("Dedusmuln", player) or self.has("Somsnosa", player) - def _hylics2_has_2_members(self, player): - return (self.has("Pongorma", player) and self.has("Dedusmuln", player)) or\ - (self.has("Pongorma", player) and self.has("Somsnosa", player)) or\ - (self.has("Dedusmuln", player) and self.has("Somsnosa", player)) +def skull_bomb(state: CollectionState, player: int) -> bool: + return state.has("SKULL BOMB", player) - def _hylics2_has_3_members(self, player): - return self.has("Pongorma", player) and self.has("Dedusmuln", player) and self.has("Somsnosa", player) - def _hylics2_enter_arcade2(self, player): - return self._hylics2_can_air_dash(player) and self._hylics2_has_airship(player) +def tower_key(state: CollectionState, player: int) -> bool: + return state.has("TOWER KEY", player) - def _hylics2_enter_wormpod(self, player): - return self._hylics2_has_airship(player) and self._hylics2_has_worm_room_key(player) and\ - self._hylics2_has_paddle(player) - def _hylics2_enter_sageship(self, player): - return self._hylics2_has_skull_bomb(player) and self._hylics2_has_airship(player) and\ - self._hylics2_has_paddle(player) +def deep_key(state: CollectionState, player: int) -> bool: + return state.has("DEEP KEY", player) - def _hylics2_enter_foglast(self, player): - return self._hylics2_enter_wormpod(player) - def _hylics2_enter_hylemxylem(self, player): - return self._hylics2_can_air_dash(player) and self._hylics2_enter_foglast(player) and\ - self._hylics2_has_bridge_key(player) +def upper_house_key(state: CollectionState, player: int) -> bool: + return state.has("UPPER HOUSE KEY", player) + + +def clicker(state: CollectionState, player: int) -> bool: + return state.has("CLICKER", player) + + +def all_tokens(state: CollectionState, player: int) -> bool: + return state.has("SAGE TOKEN", player, 3) + + +def charge_up(state: CollectionState, player: int) -> bool: + return state.has("CHARGE UP", player) + + +def paper_cup(state: CollectionState, player: int) -> bool: + return state.has("PAPER CUP", player) + + +def party_1(state: CollectionState, player: int) -> bool: + return state.has_any({"Pongorma", "Dedusmuln", "Somsnosa"}, player) + + +def party_2(state: CollectionState, player: int) -> bool: + return ( + state.has_all({"Pongorma", "Dedusmuln"}, player) + or state.has_all({"Pongorma", "Somsnosa"}, player) + or state.has_all({"Dedusmuln", "Somsnosa"}, player) + ) + + +def party_3(state: CollectionState, player: int) -> bool: + return state.has_all({"Pongorma", "Dedusmuln", "Somsnosa"}, player) + + +def enter_arcade2(state: CollectionState, player: int) -> bool: + return ( + air_dash(state, player) + and airship(state, player) + ) + + +def enter_wormpod(state: CollectionState, player: int) -> bool: + return ( + airship(state, player) + and worm_room_key(state, player) + and paddle(state, player) + ) + + +def enter_sageship(state: CollectionState, player: int) -> bool: + return ( + skull_bomb(state, player) + and airship(state, player) + and paddle(state, player) + ) + + +def enter_foglast(state: CollectionState, player: int) -> bool: + return enter_wormpod(state, player) + + +def enter_hylemxylem(state: CollectionState, player: int) -> bool: + return ( + air_dash(state, player) + and enter_foglast(state, player) + and bridge_key(state, player) + ) def set_rules(hylics2world): @@ -94,342 +131,439 @@ def set_rules(hylics2world): # Afterlife add_rule(world.get_location("Afterlife: TV", player), - lambda state: state._hylics2_has_cave_key(player)) + lambda state: cave_key(state, player)) # New Muldul add_rule(world.get_location("New Muldul: Underground Chest", player), - lambda state: state._hylics2_can_air_dash(player)) + lambda state: air_dash(state, player)) add_rule(world.get_location("New Muldul: TV", player), - lambda state: state._hylics2_has_house_key(player)) + lambda state: house_key(state, player)) add_rule(world.get_location("New Muldul: Upper House Chest 1", player), - lambda state: state._hylics2_has_upper_house_key(player)) + lambda state: upper_house_key(state, player)) add_rule(world.get_location("New Muldul: Upper House Chest 2", player), - lambda state: state._hylics2_has_upper_house_key(player)) + lambda state: upper_house_key(state, player)) add_rule(world.get_location("New Muldul: Pot above Vault", player), - lambda state: state._hylics2_can_air_dash(player)) + lambda state: air_dash(state, player)) # New Muldul Vault add_rule(world.get_location("New Muldul: Rescued Blerol 1", player), - lambda state: ((state._hylics2_has_jail_key(player) and state._hylics2_has_paddle(player)) and\ - (state._hylics2_can_air_dash(player) or state._hylics2_has_airship(player))) or\ - state._hylics2_enter_hylemxylem(player)) + lambda state: ( + ( + ( + jail_key(state, player) + and paddle(state, player) + ) + and ( + air_dash(state, player) + or airship(state, player) + ) + ) + or enter_hylemxylem(state, player) + )) add_rule(world.get_location("New Muldul: Rescued Blerol 2", player), - lambda state: ((state._hylics2_has_jail_key(player) and state._hylics2_has_paddle(player)) and\ - (state._hylics2_can_air_dash(player) or state._hylics2_has_airship(player))) or\ - state._hylics2_enter_hylemxylem(player)) + lambda state: ( + ( + ( + jail_key(state, player) + and paddle(state, player) + ) + and ( + air_dash(state, player) + or airship(state, player) + ) + ) + or enter_hylemxylem(state, player) + )) add_rule(world.get_location("New Muldul: Vault Left Chest", player), - lambda state: state._hylics2_enter_hylemxylem(player)) + lambda state: enter_hylemxylem(state, player)) add_rule(world.get_location("New Muldul: Vault Right Chest", player), - lambda state: state._hylics2_enter_hylemxylem(player)) + lambda state: enter_hylemxylem(state, player)) add_rule(world.get_location("New Muldul: Vault Bomb", player), - lambda state: state._hylics2_enter_hylemxylem(player)) + lambda state: enter_hylemxylem(state, player)) # Viewax's Edifice add_rule(world.get_location("Viewax's Edifice: Canopic Jar", player), - lambda state: state._hylics2_has_paddle(player)) + lambda state: paddle(state, player)) add_rule(world.get_location("Viewax's Edifice: Cave Sarcophagus", player), - lambda state: state._hylics2_has_paddle(player)) + lambda state: paddle(state, player)) add_rule(world.get_location("Viewax's Edifice: Shielded Key", player), - lambda state: state._hylics2_has_paddle(player)) + lambda state: paddle(state, player)) add_rule(world.get_location("Viewax's Edifice: Shielded Key", player), - lambda state: state._hylics2_has_paddle(player)) + lambda state: paddle(state, player)) add_rule(world.get_location("Viewax's Edifice: Tower Pot", player), - lambda state: state._hylics2_has_paddle(player)) + lambda state: paddle(state, player)) add_rule(world.get_location("Viewax's Edifice: Tower Jar", player), - lambda state: state._hylics2_has_paddle(player)) + lambda state: paddle(state, player)) add_rule(world.get_location("Viewax's Edifice: Tower Chest", player), - lambda state: state._hylics2_has_paddle(player) and state._hylics2_has_tower_key(player)) + lambda state: ( + paddle(state, player) + and tower_key(state, player) + )) add_rule(world.get_location("Viewax's Edifice: Viewax Pot", player), - lambda state: state._hylics2_has_paddle(player)) + lambda state: paddle(state, player)) add_rule(world.get_location("Viewax's Edifice: Defeat Viewax", player), - lambda state: state._hylics2_has_paddle(player)) + lambda state: paddle(state, player)) add_rule(world.get_location("Viewax's Edifice: TV", player), - lambda state: state._hylics2_has_paddle(player) and state._hylics2_has_jail_key(player)) + lambda state: ( + paddle(state, player) + and jail_key(state, player) + )) add_rule(world.get_location("Viewax's Edifice: Sage Fridge", player), - lambda state: state._hylics2_can_air_dash(player)) + lambda state: air_dash(state, player)) add_rule(world.get_location("Viewax's Edifice: Sage Item 1", player), - lambda state: state._hylics2_can_air_dash(player)) + lambda state: air_dash(state, player)) add_rule(world.get_location("Viewax's Edifice: Sage Item 2", player), - lambda state: state._hylics2_can_air_dash(player)) + lambda state: air_dash(state, player)) # Arcade 1 add_rule(world.get_location("Arcade 1: Key", player), - lambda state: state._hylics2_has_paddle(player)) + lambda state: paddle(state, player)) add_rule(world.get_location("Arcade 1: Coin Dash", player), - lambda state: state._hylics2_has_paddle(player)) + lambda state: paddle(state, player)) add_rule(world.get_location("Arcade 1: Burrito Alcove 1", player), - lambda state: state._hylics2_has_paddle(player)) + lambda state: paddle(state, player)) add_rule(world.get_location("Arcade 1: Burrito Alcove 2", player), - lambda state: state._hylics2_has_paddle(player)) + lambda state: paddle(state, player)) add_rule(world.get_location("Arcade 1: Behind Spikes Banana", player), - lambda state: state._hylics2_has_paddle(player)) + lambda state: paddle(state, player)) add_rule(world.get_location("Arcade 1: Pyramid Banana", player), - lambda state: state._hylics2_has_paddle(player)) + lambda state: paddle(state, player)) add_rule(world.get_location("Arcade 1: Moving Platforms Muscle Applique", player), - lambda state: state._hylics2_has_paddle(player)) + lambda state: paddle(state, player)) add_rule(world.get_location("Arcade 1: Bed Banana", player), - lambda state: state._hylics2_has_paddle(player)) + lambda state: paddle(state, player)) # Airship add_rule(world.get_location("Airship: Talk to Somsnosa", player), - lambda state: state._hylics2_has_worm_room_key(player)) + lambda state: worm_room_key(state, player)) # Foglast add_rule(world.get_location("Foglast: Underground Sarcophagus", player), - lambda state: state._hylics2_can_air_dash(player)) + lambda state: air_dash(state, player)) add_rule(world.get_location("Foglast: Shielded Key", player), - lambda state: state._hylics2_can_air_dash(player)) + lambda state: air_dash(state, player)) add_rule(world.get_location("Foglast: TV", player), - lambda state: state._hylics2_can_air_dash(player) and state._hylics2_has_clicker(player)) + lambda state: ( + air_dash(state, player) + and clicker(state, player) + )) add_rule(world.get_location("Foglast: Buy Clicker", player), - lambda state: state._hylics2_can_air_dash(player)) + lambda state: air_dash(state, player)) add_rule(world.get_location("Foglast: Shielded Chest", player), - lambda state: state._hylics2_can_air_dash(player)) + lambda state: air_dash(state, player)) add_rule(world.get_location("Foglast: Cave Fridge", player), - lambda state: state._hylics2_can_air_dash(player)) + lambda state: air_dash(state, player)) add_rule(world.get_location("Foglast: Roof Sarcophagus", player), - lambda state: state._hylics2_can_air_dash(player) and state._hylics2_has_bridge_key(player)) + lambda state: ( + air_dash(state, player) + and bridge_key(state, player) + )) add_rule(world.get_location("Foglast: Under Lair Sarcophagus 1", player), - lambda state: state._hylics2_can_air_dash(player) and state._hylics2_has_bridge_key(player)) + lambda state: ( + air_dash(state, player) + and bridge_key(state, player) + )) add_rule(world.get_location("Foglast: Under Lair Sarcophagus 2", player), - lambda state: state._hylics2_can_air_dash(player) and state._hylics2_has_bridge_key(player)) + lambda state: ( + air_dash(state, player) + and bridge_key(state, player) + )) add_rule(world.get_location("Foglast: Under Lair Sarcophagus 3", player), - lambda state: state._hylics2_can_air_dash(player) and state._hylics2_has_bridge_key(player)) + lambda state: ( + air_dash(state, player) + and bridge_key(state, player) + )) add_rule(world.get_location("Foglast: Sage Sarcophagus", player), - lambda state: state._hylics2_can_air_dash(player) and state._hylics2_has_bridge_key(player)) + lambda state: ( + air_dash(state, player) + and bridge_key(state, player) + )) add_rule(world.get_location("Foglast: Sage Item 1", player), - lambda state: state._hylics2_can_air_dash(player) and state._hylics2_has_bridge_key(player)) + lambda state: ( + air_dash(state, player) + and bridge_key(state, player) + )) add_rule(world.get_location("Foglast: Sage Item 2", player), - lambda state: state._hylics2_can_air_dash(player) and state._hylics2_has_bridge_key(player)) + lambda state: ( + air_dash(state, player) + and bridge_key(state, player) + )) # Drill Castle add_rule(world.get_location("Drill Castle: Island Banana", player), - lambda state: state._hylics2_can_air_dash(player)) + lambda state: air_dash(state, player)) add_rule(world.get_location("Drill Castle: Island Pot", player), - lambda state: state._hylics2_can_air_dash(player)) + lambda state: air_dash(state, player)) add_rule(world.get_location("Drill Castle: Cave Sarcophagus", player), - lambda state: state._hylics2_can_air_dash(player)) + lambda state: air_dash(state, player)) add_rule(world.get_location("Drill Castle: TV", player), - lambda state: state._hylics2_can_air_dash(player)) + lambda state: air_dash(state, player)) # Sage Labyrinth add_rule(world.get_location("Sage Labyrinth: Sage Item 1", player), - lambda state: state._hylics2_has_deep_key(player)) + lambda state: deep_key(state, player)) add_rule(world.get_location("Sage Labyrinth: Sage Item 2", player), - lambda state: state._hylics2_has_deep_key(player)) + lambda state: deep_key(state, player)) add_rule(world.get_location("Sage Labyrinth: Sage Left Arm", player), - lambda state: state._hylics2_has_deep_key(player)) + lambda state: deep_key(state, player)) add_rule(world.get_location("Sage Labyrinth: Sage Right Arm", player), - lambda state: state._hylics2_has_deep_key(player)) + lambda state: deep_key(state, player)) add_rule(world.get_location("Sage Labyrinth: Sage Left Leg", player), - lambda state: state._hylics2_has_deep_key(player)) + lambda state: deep_key(state, player)) add_rule(world.get_location("Sage Labyrinth: Sage Right Leg", player), - lambda state: state._hylics2_has_deep_key(player)) + lambda state: deep_key(state, player)) # Sage Airship add_rule(world.get_location("Sage Airship: TV", player), - lambda state: state._hylics2_has_tokens(player)) + lambda state: all_tokens(state, player)) # Hylemxylem add_rule(world.get_location("Hylemxylem: Upper Chamber Banana", player), - lambda state: state._hylics2_has_upper_chamber_key(player)) + lambda state: upper_chamber_key(state, player)) add_rule(world.get_location("Hylemxylem: Across Upper Reservoir Chest", player), - lambda state: state._hylics2_has_upper_chamber_key(player)) + lambda state: upper_chamber_key(state, player)) add_rule(world.get_location("Hylemxylem: Drained Lower Reservoir Chest", player), - lambda state: state._hylics2_has_upper_chamber_key(player)) + lambda state: upper_chamber_key(state, player)) add_rule(world.get_location("Hylemxylem: Drained Lower Reservoir Burrito 1", player), - lambda state: state._hylics2_has_upper_chamber_key(player)) + lambda state: upper_chamber_key(state, player)) add_rule(world.get_location("Hylemxylem: Drained Lower Reservoir Burrito 2", player), - lambda state: state._hylics2_has_upper_chamber_key(player)) + lambda state: upper_chamber_key(state, player)) add_rule(world.get_location("Hylemxylem: Lower Reservoir Hole Pot 1", player), - lambda state: state._hylics2_has_upper_chamber_key(player)) + lambda state: upper_chamber_key(state, player)) add_rule(world.get_location("Hylemxylem: Lower Reservoir Hole Pot 2", player), - lambda state: state._hylics2_has_upper_chamber_key(player)) + lambda state: upper_chamber_key(state, player)) add_rule(world.get_location("Hylemxylem: Lower Reservoir Hole Pot 3", player), - lambda state: state._hylics2_has_upper_chamber_key(player)) + lambda state: upper_chamber_key(state, player)) add_rule(world.get_location("Hylemxylem: Lower Reservoir Hole Sarcophagus", player), - lambda state: state._hylics2_has_upper_chamber_key(player)) + lambda state: upper_chamber_key(state, player)) add_rule(world.get_location("Hylemxylem: Drained Upper Reservoir Burrito 1", player), - lambda state: state._hylics2_has_upper_chamber_key(player)) + lambda state: upper_chamber_key(state, player)) add_rule(world.get_location("Hylemxylem: Drained Upper Reservoir Burrito 2", player), - lambda state: state._hylics2_has_upper_chamber_key(player)) + lambda state: upper_chamber_key(state, player)) add_rule(world.get_location("Hylemxylem: Drained Upper Reservoir Burrito 3", player), - lambda state: state._hylics2_has_upper_chamber_key(player)) + lambda state: upper_chamber_key(state, player)) add_rule(world.get_location("Hylemxylem: Upper Reservoir Hole Key", player), - lambda state: state._hylics2_has_upper_chamber_key(player)) + lambda state: upper_chamber_key(state, player)) # extra rules if Extra Items in Logic is enabled if world.extra_items_in_logic[player]: for i in world.get_region("Foglast", player).entrances: - add_rule(i, lambda state: state._hylics2_has_charge_up(player)) + add_rule(i, lambda state: charge_up(state, player)) for i in world.get_region("Sage Airship", player).entrances: - add_rule(i, lambda state: state._hylics2_has_charge_up(player) and state._hylics2_has_cup(player) and\ - state._hylics2_has_worm_room_key(player)) + add_rule(i, lambda state: ( + charge_up(state, player) + and paper_cup(state, player) + and worm_room_key(state, player) + )) for i in world.get_region("Hylemxylem", player).entrances: - add_rule(i, lambda state: state._hylics2_has_charge_up(player) and state._hylics2_has_cup(player)) + add_rule(i, lambda state: ( + charge_up(state, player) + and paper_cup(state, player) + )) add_rule(world.get_location("Sage Labyrinth: Motor Hunter Sarcophagus", player), - lambda state: state._hylics2_has_charge_up(player) and state._hylics2_has_cup(player)) + lambda state: ( + charge_up(state, player) + and paper_cup(state, player) + )) # extra rules if Shuffle Party Members is enabled if world.party_shuffle[player]: for i in world.get_region("Arcade Island", player).entrances: - add_rule(i, lambda state: state._hylics2_has_3_members(player)) + add_rule(i, lambda state: party_3(state, player)) for i in world.get_region("Foglast", player).entrances: - add_rule(i, lambda state: state._hylics2_has_3_members(player) or\ - (state._hylics2_has_2_members(player) and state._hylics2_has_jail_key(player))) + add_rule(i, lambda state: ( + party_3(state, player) + or ( + party_2(state, player) + and jail_key(state, player) + ) + )) for i in world.get_region("Sage Airship", player).entrances: - add_rule(i, lambda state: state._hylics2_has_3_members(player)) + add_rule(i, lambda state: party_3(state, player)) for i in world.get_region("Hylemxylem", player).entrances: - add_rule(i, lambda state: state._hylics2_has_3_members(player)) + add_rule(i, lambda state: party_3(state, player)) add_rule(world.get_location("Viewax's Edifice: Defeat Viewax", player), - lambda state: state._hylics2_has_2_members(player)) + lambda state: party_2(state, player)) add_rule(world.get_location("New Muldul: Rescued Blerol 1", player), - lambda state: state._hylics2_has_2_members(player)) + lambda state: party_2(state, player)) add_rule(world.get_location("New Muldul: Rescued Blerol 2", player), - lambda state: state._hylics2_has_2_members(player)) + lambda state: party_2(state, player)) add_rule(world.get_location("New Muldul: Vault Left Chest", player), - lambda state: state._hylics2_has_3_members(player)) + lambda state: party_3(state, player)) add_rule(world.get_location("New Muldul: Vault Right Chest", player), - lambda state: state._hylics2_has_3_members(player)) + lambda state: party_3(state, player)) add_rule(world.get_location("New Muldul: Vault Bomb", player), - lambda state: state._hylics2_has_3_members(player)) + lambda state: party_3(state, player)) add_rule(world.get_location("Juice Ranch: Battle with Somsnosa", player), - lambda state: state._hylics2_has_2_members(player)) + lambda state: party_2(state, player)) add_rule(world.get_location("Juice Ranch: Somsnosa Joins", player), - lambda state: state._hylics2_has_2_members(player)) + lambda state: party_2(state, player)) add_rule(world.get_location("Airship: Talk to Somsnosa", player), - lambda state: state._hylics2_has_3_members(player)) + lambda state: party_3(state, player)) add_rule(world.get_location("Sage Labyrinth: Motor Hunter Sarcophagus", player), - lambda state: state._hylics2_has_3_members(player)) + lambda state: party_3(state, player)) # extra rules if Shuffle Red Medallions is enabled if world.medallion_shuffle[player]: add_rule(world.get_location("New Muldul: Upper House Medallion", player), - lambda state: state._hylics2_has_upper_house_key(player)) + lambda state: upper_house_key(state, player)) add_rule(world.get_location("New Muldul: Vault Rear Left Medallion", player), - lambda state: state._hylics2_enter_foglast(player) and state._hylics2_has_bridge_key(player)) + lambda state: ( + enter_foglast(state, player) + and bridge_key(state, player) + )) add_rule(world.get_location("New Muldul: Vault Rear Right Medallion", player), - lambda state: state._hylics2_enter_foglast(player) and state._hylics2_has_bridge_key(player)) + lambda state: ( + enter_foglast(state, player) + and bridge_key(state, player) + )) add_rule(world.get_location("New Muldul: Vault Center Medallion", player), - lambda state: state._hylics2_enter_foglast(player) and state._hylics2_has_bridge_key(player)) + lambda state: ( + enter_foglast(state, player) + and bridge_key(state, player) + )) add_rule(world.get_location("New Muldul: Vault Front Left Medallion", player), - lambda state: state._hylics2_enter_foglast(player) and state._hylics2_has_bridge_key(player)) + lambda state: ( + enter_foglast(state, player) + and bridge_key(state, player) + )) add_rule(world.get_location("New Muldul: Vault Front Right Medallion", player), - lambda state: state._hylics2_enter_foglast(player) and state._hylics2_has_bridge_key(player)) + lambda state: ( + enter_foglast(state, player) + and bridge_key(state, player) + )) add_rule(world.get_location("Viewax's Edifice: Fort Wall Medallion", player), - lambda state: state._hylics2_has_paddle(player)) + lambda state: paddle(state, player)) add_rule(world.get_location("Viewax's Edifice: Jar Medallion", player), - lambda state: state._hylics2_has_paddle(player)) + lambda state: paddle(state, player)) add_rule(world.get_location("Viewax's Edifice: Sage Chair Medallion", player), - lambda state: state._hylics2_can_air_dash(player)) + lambda state: air_dash(state, player)) add_rule(world.get_location("Arcade 1: Lonely Medallion", player), - lambda state: state._hylics2_has_paddle(player)) + lambda state: paddle(state, player)) add_rule(world.get_location("Arcade 1: Alcove Medallion", player), - lambda state: state._hylics2_has_paddle(player)) + lambda state: paddle(state, player)) add_rule(world.get_location("Foglast: Under Lair Medallion", player), - lambda state: state._hylics2_has_bridge_key(player)) + lambda state: bridge_key(state, player)) add_rule(world.get_location("Foglast: Mid-Air Medallion", player), - lambda state: state._hylics2_can_air_dash(player)) + lambda state: air_dash(state, player)) add_rule(world.get_location("Foglast: Top of Tower Medallion", player), - lambda state: state._hylics2_has_paddle(player)) + lambda state: paddle(state, player)) add_rule(world.get_location("Hylemxylem: Lower Reservoir Hole Medallion", player), - lambda state: state._hylics2_has_upper_chamber_key(player)) + lambda state: upper_chamber_key(state, player)) - # extra rules is Shuffle Red Medallions and Party Shuffle are enabled + # extra rules if Shuffle Red Medallions and Party Shuffle are enabled if world.party_shuffle[player] and world.medallion_shuffle[player]: add_rule(world.get_location("New Muldul: Vault Rear Left Medallion", player), - lambda state: state._hylics2_has_3_members(player)) + lambda state: party_3(state, player)) add_rule(world.get_location("New Muldul: Vault Rear Right Medallion", player), - lambda state: state._hylics2_has_3_members(player)) + lambda state: party_3(state, player)) add_rule(world.get_location("New Muldul: Vault Center Medallion", player), - lambda state: state._hylics2_has_3_members(player)) + lambda state: party_3(state, player)) add_rule(world.get_location("New Muldul: Vault Front Left Medallion", player), - lambda state: state._hylics2_has_3_members(player)) + lambda state: party_3(state, player)) add_rule(world.get_location("New Muldul: Vault Front Right Medallion", player), - lambda state: state._hylics2_has_3_members(player)) + lambda state: party_3(state, player)) # entrances for i in world.get_region("Airship", player).entrances: - add_rule(i, lambda state: state._hylics2_has_airship(player)) + add_rule(i, lambda state: airship(state, player)) for i in world.get_region("Arcade Island", player).entrances: - add_rule(i, lambda state: state._hylics2_has_airship(player) and state._hylics2_can_air_dash(player)) + add_rule(i, lambda state: ( + airship(state, player) + and air_dash(state, player) + )) for i in world.get_region("Worm Pod", player).entrances: - add_rule(i, lambda state: state._hylics2_enter_wormpod(player)) + add_rule(i, lambda state: enter_wormpod(state, player)) for i in world.get_region("Foglast", player).entrances: - add_rule(i, lambda state: state._hylics2_enter_foglast(player)) + add_rule(i, lambda state: enter_foglast(state, player)) for i in world.get_region("Sage Labyrinth", player).entrances: - add_rule(i, lambda state: state._hylics2_has_skull_bomb(player)) + add_rule(i, lambda state: skull_bomb(state, player)) for i in world.get_region("Sage Airship", player).entrances: - add_rule(i, lambda state: state._hylics2_enter_sageship(player)) + add_rule(i, lambda state: enter_sageship(state, player)) for i in world.get_region("Hylemxylem", player).entrances: - add_rule(i, lambda state: state._hylics2_enter_hylemxylem(player)) + add_rule(i, lambda state: enter_hylemxylem(state, player)) # random start logic (default) if ((not world.random_start[player]) or \ (world.random_start[player] and hylics2world.start_location == "Waynehouse")): # entrances for i in world.get_region("Viewax", player).entrances: - add_rule(i, lambda state: state._hylics2_can_air_dash(player) or state._hylics2_has_airship(player)) + add_rule(i, lambda state: ( + air_dash(state, player) + and airship(state, player) + )) for i in world.get_region("TV Island", player).entrances: - add_rule(i, lambda state: state._hylics2_has_airship(player)) + add_rule(i, lambda state: airship(state, player)) for i in world.get_region("Shield Facility", player).entrances: - add_rule(i, lambda state: state._hylics2_has_airship(player)) + add_rule(i, lambda state: airship(state, player)) for i in world.get_region("Juice Ranch", player).entrances: - add_rule(i, lambda state: state._hylics2_has_airship(player)) + add_rule(i, lambda state: airship(state, player)) # random start logic (Viewax's Edifice) elif (world.random_start[player] and hylics2world.start_location == "Viewax's Edifice"): for i in world.get_region("Waynehouse", player).entrances: - add_rule(i, lambda state: state._hylics2_can_air_dash(player) or state._hylics2_has_airship(player)) + add_rule(i, lambda state: ( + air_dash(state, player) + or airship(state, player) + )) for i in world.get_region("New Muldul", player).entrances: - add_rule(i, lambda state: state._hylics2_can_air_dash(player) or state._hylics2_has_airship(player)) + add_rule(i, lambda state: ( + air_dash(state, player) + or airship(state, player) + )) for i in world.get_region("New Muldul Vault", player).entrances: - add_rule(i, lambda state: state._hylics2_can_air_dash(player) or state._hylics2_has_airship(player)) + add_rule(i, lambda state: ( + air_dash(state, player) + or airship(state, player) + )) for i in world.get_region("Drill Castle", player).entrances: - add_rule(i, lambda state: state._hylics2_can_air_dash(player) or state._hylics2_has_airship(player)) + add_rule(i, lambda state: ( + air_dash(state, player) + or airship(state, player) + )) for i in world.get_region("TV Island", player).entrances: - add_rule(i, lambda state: state._hylics2_has_airship(player)) + add_rule(i, lambda state: airship(state, player)) for i in world.get_region("Shield Facility", player).entrances: - add_rule(i, lambda state: state._hylics2_has_airship(player)) + add_rule(i, lambda state: airship(state, player)) for i in world.get_region("Juice Ranch", player).entrances: - add_rule(i, lambda state: state._hylics2_has_airship(player)) + add_rule(i, lambda state: airship(state, player)) for i in world.get_region("Sage Labyrinth", player).entrances: - add_rule(i, lambda state: state._hylics2_has_airship(player)) + add_rule(i, lambda state: airship(state, player)) # random start logic (TV Island) elif (world.random_start[player] and hylics2world.start_location == "TV Island"): for i in world.get_region("Waynehouse", player).entrances: - add_rule(i, lambda state: state._hylics2_has_airship(player)) + add_rule(i, lambda state: airship(state, player)) for i in world.get_region("New Muldul", player).entrances: - add_rule(i, lambda state: state._hylics2_has_airship(player)) + add_rule(i, lambda state: airship(state, player)) for i in world.get_region("New Muldul Vault", player).entrances: - add_rule(i, lambda state: state._hylics2_has_airship(player)) + add_rule(i, lambda state: airship(state, player)) for i in world.get_region("Drill Castle", player).entrances: - add_rule(i, lambda state: state._hylics2_has_airship(player)) + add_rule(i, lambda state: airship(state, player)) for i in world.get_region("Viewax", player).entrances: - add_rule(i, lambda state: state._hylics2_has_airship(player)) + add_rule(i, lambda state: airship(state, player)) for i in world.get_region("Shield Facility", player).entrances: - add_rule(i, lambda state: state._hylics2_has_airship(player)) + add_rule(i, lambda state: airship(state, player)) for i in world.get_region("Juice Ranch", player).entrances: - add_rule(i, lambda state: state._hylics2_has_airship(player)) + add_rule(i, lambda state: airship(state, player)) for i in world.get_region("Sage Labyrinth", player).entrances: - add_rule(i, lambda state: state._hylics2_has_airship(player)) + add_rule(i, lambda state: airship(state, player)) # random start logic (Shield Facility) elif (world.random_start[player] and hylics2world.start_location == "Shield Facility"): for i in world.get_region("Waynehouse", player).entrances: - add_rule(i, lambda state: state._hylics2_has_airship(player)) + add_rule(i, lambda state: airship(state, player)) for i in world.get_region("New Muldul", player).entrances: - add_rule(i, lambda state: state._hylics2_has_airship(player)) + add_rule(i, lambda state: airship(state, player)) for i in world.get_region("New Muldul Vault", player).entrances: - add_rule(i, lambda state: state._hylics2_has_airship(player)) + add_rule(i, lambda state: airship(state, player)) for i in world.get_region("Drill Castle", player).entrances: - add_rule(i, lambda state: state._hylics2_has_airship(player)) + add_rule(i, lambda state: airship(state, player)) for i in world.get_region("Viewax", player).entrances: - add_rule(i, lambda state: state._hylics2_has_airship(player)) + add_rule(i, lambda state: airship(state, player)) for i in world.get_region("TV Island", player).entrances: - add_rule(i, lambda state: state._hylics2_has_airship(player)) + add_rule(i, lambda state: airship(state, player)) for i in world.get_region("Sage Labyrinth", player).entrances: - add_rule(i, lambda state: state._hylics2_has_airship(player)) + add_rule(i, lambda state: airship(state, player)) \ No newline at end of file diff --git a/worlds/hylics2/__init__.py b/worlds/hylics2/__init__.py index f721fb4749..19d901bf5a 100644 --- a/worlds/hylics2/__init__.py +++ b/worlds/hylics2/__init__.py @@ -130,11 +130,11 @@ class Hylics2World(World): tvs = list(Locations.tv_location_table.items()) # if Extra Items in Logic is enabled place CHARGE UP first and make sure it doesn't get - # placed at Sage Airship: TV + # placed at Sage Airship: TV or Foglast: TV if self.multiworld.extra_items_in_logic[self.player]: tv = self.multiworld.random.choice(tvs) gest = gestures.index((200681, Items.gesture_item_table[200681])) - while tv[1]["name"] == "Sage Airship: TV": + while tv[1]["name"] == "Sage Airship: TV" or tv[1]["name"] == "Foglast: TV": tv = self.multiworld.random.choice(tvs) self.multiworld.get_location(tv[1]["name"], self.player)\ .place_locked_item(self.add_item(gestures[gest][1]["name"], gestures[gest][1]["classification"], @@ -146,7 +146,7 @@ class Hylics2World(World): gest = self.multiworld.random.choice(gestures) tv = self.multiworld.random.choice(tvs) self.multiworld.get_location(tv[1]["name"], self.player)\ - .place_locked_item(self.add_item(gest[1]["name"], gest[1]["classification"], gest[1])) + .place_locked_item(self.add_item(gest[1]["name"], gest[1]["classification"], gest[0])) gestures.remove(gest) tvs.remove(tv) @@ -232,8 +232,10 @@ class Hylics2World(World): # create location for beating the game and place Victory event there loc = Location(self.player, "Defeat Gibby", None, self.multiworld.get_region("Hylemxylem", self.player)) loc.place_locked_item(self.create_event("Victory")) - set_rule(loc, lambda state: state._hylics2_has_upper_chamber_key(self.player) - and state._hylics2_has_vessel_room_key(self.player)) + set_rule(loc, lambda state: ( + state.has("UPPER CHAMBER KEY", self.player) + and state.has("VESSEL ROOM KEY", self.player) + )) self.multiworld.get_region("Hylemxylem", self.player).locations.append(loc) self.multiworld.completion_condition[self.player] = lambda state: state.has("Victory", self.player) From 5ca1ababfdd94a6e309d97f658065aeb86b7a463 Mon Sep 17 00:00:00 2001 From: agilbert1412 Date: Wed, 18 Oct 2023 15:53:12 -0400 Subject: [PATCH 10/54] DLC Quest: Fix code structure, typos, poor code quality (#2066) "Added a bunch of tests to make sure I don't break anything during refactoring Huge cleanup in the Regions file, extract methods, remove code duplicate, fix typos, fix variable naming conventions, etc. Small cleanup in other places, minor stuff just what was needed for Regions" --- worlds/dlcquest/Regions.py | 415 +++++++------------- worlds/dlcquest/test/TestItemShuffle.py | 130 ++++++ worlds/dlcquest/test/TestOptionsLong.py | 87 ++++ worlds/dlcquest/test/__init__.py | 53 +++ worlds/dlcquest/test/checks/__init__.py | 0 worlds/dlcquest/test/checks/world_checks.py | 42 ++ worlds/dlcquest/test/option_names.py | 5 + 7 files changed, 455 insertions(+), 277 deletions(-) create mode 100644 worlds/dlcquest/test/TestItemShuffle.py create mode 100644 worlds/dlcquest/test/TestOptionsLong.py create mode 100644 worlds/dlcquest/test/__init__.py create mode 100644 worlds/dlcquest/test/checks/__init__.py create mode 100644 worlds/dlcquest/test/checks/world_checks.py create mode 100644 worlds/dlcquest/test/option_names.py diff --git a/worlds/dlcquest/Regions.py b/worlds/dlcquest/Regions.py index dfb5f6c021..402ac722a0 100644 --- a/worlds/dlcquest/Regions.py +++ b/worlds/dlcquest/Regions.py @@ -1,4 +1,5 @@ import math +from typing import List from BaseClasses import Entrance, MultiWorld, Region from . import Options @@ -9,318 +10,178 @@ DLCQuestRegion = ["Movement Pack", "Behind Tree", "Psychological Warfare", "Doub "Double Jump Behind the Tree", "The Forest", "Final Room"] -def add_coin_freemium(region: Region, Coin: int, player: int): - number_coin = f"{Coin} coins freemium" - location_coin = f"{region.name} coins freemium" +def add_coin_lfod(region: Region, coin: int, player: int): + add_coin(region, coin, player, " coins freemium") + + +def add_coin_dlcquest(region: Region, coin: int, player: int): + add_coin(region, coin, player, " coins") + + +def add_coin(region: Region, coin: int, player: int, suffix: str): + number_coin = f"{coin}{suffix}" + location_coin = f"{region.name}{suffix}" location = DLCQuestLocation(player, location_coin, None, region) region.locations.append(location) location.place_locked_item(create_event(player, number_coin)) -def add_coin_dlcquest(region: Region, Coin: int, player: int): - number_coin = f"{Coin} coins" - location_coin = f"{region.name} coins" - location = DLCQuestLocation(player, location_coin, None, region) - region.locations.append(location) - location.place_locked_item(create_event(player, number_coin)) +def create_regions(multiworld: MultiWorld, player: int, world_options: Options.DLCQuestOptions): + region_menu = Region("Menu", player, multiworld) + has_campaign_basic = world_options.campaign == Options.Campaign.option_basic or world_options.campaign == Options.Campaign.option_both + has_campaign_lfod = world_options.campaign == Options.Campaign.option_live_freemium_or_die or world_options.campaign == Options.Campaign.option_both + has_coinsanity = world_options.coinsanity == Options.CoinSanity.option_coin + coin_bundle_size = world_options.coinbundlequantity.value + has_item_shuffle = world_options.item_shuffle == Options.ItemShuffle.option_shuffled + + multiworld.regions.append(region_menu) + + create_regions_basic_campaign(has_campaign_basic, region_menu, has_item_shuffle, has_coinsanity, coin_bundle_size, player, multiworld) + + create_regions_lfod_campaign(coin_bundle_size, has_campaign_lfod, has_coinsanity, has_item_shuffle, multiworld, player, region_menu) -def create_regions(world: MultiWorld, player: int, World_Options: Options.DLCQuestOptions): - Regmenu = Region("Menu", player, world) - if (World_Options.campaign == Options.Campaign.option_basic or World_Options.campaign - == Options.Campaign.option_both): - Regmenu.exits += [Entrance(player, "DLC Quest Basic", Regmenu)] - if (World_Options.campaign == Options.Campaign.option_live_freemium_or_die or World_Options.campaign - == Options.Campaign.option_both): - Regmenu.exits += [Entrance(player, "Live Freemium or Die", Regmenu)] - world.regions.append(Regmenu) +def create_regions_basic_campaign(has_campaign_basic: bool, region_menu: Region, has_item_shuffle: bool, has_coinsanity: bool, + coin_bundle_size: int, player: int, world: MultiWorld): + if not has_campaign_basic: + return - if (World_Options.campaign == Options.Campaign.option_basic or World_Options.campaign - == Options.Campaign.option_both): + region_menu.exits += [Entrance(player, "DLC Quest Basic", region_menu)] + locations_move_right = ["Movement Pack", "Animation Pack", "Audio Pack", "Pause Menu Pack"] + region_move_right = create_region_and_locations_basic("Move Right", locations_move_right, ["Moving"], player, world, 4) + create_coinsanity_locations_dlc_quest(has_coinsanity, coin_bundle_size, player, region_move_right) + locations_movement_pack = ["Time is Money Pack", "Psychological Warfare Pack", "Armor for your Horse Pack", "Shepherd Sheep"] + locations_movement_pack += conditional_location(has_item_shuffle, "Sword") + create_region_and_locations_basic("Movement Pack", locations_movement_pack, ["Tree", "Cloud"], player, world, 46) + locations_behind_tree = ["Double Jump Pack", "Map Pack", "Between Trees Sheep", "Hole in the Wall Sheep"] + conditional_location(has_item_shuffle, "Gun") + create_region_and_locations_basic("Behind Tree", locations_behind_tree, ["Behind Tree Double Jump", "Forest Entrance"], player, world, 60) + create_region_and_locations_basic("Psychological Warfare", ["West Cave Sheep"], ["Cloud Double Jump"], player, world, 100) + locations_double_jump_left = ["Pet Pack", "Top Hat Pack", "North West Alcove Sheep"] + create_region_and_locations_basic("Double Jump Total Left", locations_double_jump_left, ["Cave Tree", "Cave Roof"], player, world, 50) + create_region_and_locations_basic("Double Jump Total Left Cave", ["Top Hat Sheep"], [], player, world, 9) + create_region_and_locations_basic("Double Jump Total Left Roof", ["North West Ceiling Sheep"], [], player, world, 10) + locations_double_jump_left_ceiling = ["Sexy Outfits Pack", "Double Jump Alcove Sheep", "Sexy Outfits Sheep"] + create_region_and_locations_basic("Double Jump Behind Tree", locations_double_jump_left_ceiling, ["True Double Jump"], player, world, 89) + create_region_and_locations_basic("True Double Jump Behind Tree", ["Double Jump Floating Sheep", "Cutscene Sheep"], [], player, world, 7) + create_region_and_locations_basic("The Forest", ["Gun Pack", "Night Map Pack"], ["Behind Ogre", "Forest Double Jump"], player, world, 171) + create_region_and_locations_basic("The Forest with double Jump", ["The Zombie Pack", "Forest Low Sheep"], ["Forest True Double Jump"], player, world, 76) + create_region_and_locations_basic("The Forest with double Jump Part 2", ["Forest High Sheep"], [], player, world, 203) + region_final_boss_room = create_region_and_locations_basic("The Final Boss Room", ["Finish the Fight Pack"], [], player, world) - Regmoveright = Region("Move Right", player, world, "Start of the basic game") - Locmoveright_name = ["Movement Pack", "Animation Pack", "Audio Pack", "Pause Menu Pack"] - Regmoveright.exits = [Entrance(player, "Moving", Regmoveright)] - Regmoveright.locations += [DLCQuestLocation(player, loc_name, location_table[loc_name], Regmoveright) for - loc_name in Locmoveright_name] - add_coin_dlcquest(Regmoveright, 4, player) - if World_Options.coinsanity == Options.CoinSanity.option_coin: - coin_bundle_needed = math.floor(825 / World_Options.coinbundlequantity) - for i in range(coin_bundle_needed): - item_coin = f"DLC Quest: {World_Options.coinbundlequantity * (i + 1)} Coin" - Regmoveright.locations += [ - DLCQuestLocation(player, item_coin, location_table[item_coin], Regmoveright)] - if 825 % World_Options.coinbundlequantity != 0: - Regmoveright.locations += [ - DLCQuestLocation(player, "DLC Quest: 825 Coin", location_table["DLC Quest: 825 Coin"], - Regmoveright)] - world.regions.append(Regmoveright) + create_victory_event(region_final_boss_room, "Winning Basic", "Victory Basic", player) - Regmovpack = Region("Movement Pack", player, world) - Locmovpack_name = ["Time is Money Pack", "Psychological Warfare Pack", "Armor for your Horse Pack", - "Shepherd Sheep"] - if World_Options.item_shuffle == Options.ItemShuffle.option_shuffled: - Locmovpack_name += ["Sword"] - Regmovpack.exits = [Entrance(player, "Tree", Regmovpack), Entrance(player, "Cloud", Regmovpack)] - Regmovpack.locations += [DLCQuestLocation(player, loc_name, location_table[loc_name], Regmovpack) for loc_name - in Locmovpack_name] - add_coin_dlcquest(Regmovpack, 46, player) - world.regions.append(Regmovpack) + connect_entrances_basic(player, world) - Regbtree = Region("Behind Tree", player, world) - Locbtree_name = ["Double Jump Pack", "Map Pack", "Between Trees Sheep", "Hole in the Wall Sheep"] - if World_Options.item_shuffle == Options.ItemShuffle.option_shuffled: - Locbtree_name += ["Gun"] - Regbtree.exits = [Entrance(player, "Behind Tree Double Jump", Regbtree), - Entrance(player, "Forest Entrance", Regbtree)] - Regbtree.locations += [DLCQuestLocation(player, loc_name, location_table[loc_name], Regbtree) for loc_name in - Locbtree_name] - add_coin_dlcquest(Regbtree, 60, player) - world.regions.append(Regbtree) - Regpsywarfare = Region("Psychological Warfare", player, world) - Locpsywarfare_name = ["West Cave Sheep"] - Regpsywarfare.exits = [Entrance(player, "Cloud Double Jump", Regpsywarfare)] - Regpsywarfare.locations += [DLCQuestLocation(player, loc_name, location_table[loc_name], Regpsywarfare) for - loc_name in Locpsywarfare_name] - add_coin_dlcquest(Regpsywarfare, 100, player) - world.regions.append(Regpsywarfare) +def create_regions_lfod_campaign(coin_bundle_size, has_campaign_lfod, has_coinsanity, has_item_shuffle, multiworld, player, region_menu): + if not has_campaign_lfod: + return - Regdoubleleft = Region("Double Jump Total Left", player, world) - Locdoubleleft_name = ["Pet Pack", "Top Hat Pack", "North West Alcove Sheep"] - Regdoubleleft.locations += [DLCQuestLocation(player, loc_name, location_table[loc_name], Regdoubleleft) for - loc_name in - Locdoubleleft_name] - Regdoubleleft.exits = [Entrance(player, "Cave Tree", Regdoubleleft), - Entrance(player, "Cave Roof", Regdoubleleft)] - add_coin_dlcquest(Regdoubleleft, 50, player) - world.regions.append(Regdoubleleft) + region_menu.exits += [Entrance(player, "Live Freemium or Die", region_menu)] + locations_lfod_start = ["Particles Pack", "Day One Patch Pack", "Checkpoint Pack", "Incredibly Important Pack", + "Nice Try", "Story is Important", "I Get That Reference!"] + conditional_location(has_item_shuffle, "Wooden Sword") + region_lfod_start = create_region_and_locations_lfod("Freemium Start", locations_lfod_start, ["Vines"], player, multiworld, 50) + create_coinsanity_locations_lfod(has_coinsanity, coin_bundle_size, player, region_lfod_start) + locations_behind_vines = ["Wall Jump Pack", "Health Bar Pack", "Parallax Pack"] + conditional_location(has_item_shuffle, "Pickaxe") + create_region_and_locations_lfod("Behind the Vines", locations_behind_vines, ["Wall Jump Entrance"], player, multiworld, 95) + locations_wall_jump = ["Harmless Plants Pack", "Death of Comedy Pack", "Canadian Dialog Pack", "DLC NPC Pack"] + create_region_and_locations_lfod("Wall Jump", locations_wall_jump, ["Harmless Plants", "Pickaxe Hard Cave"], player, multiworld, 150) + create_region_and_locations_lfod("Fake Ending", ["Cut Content Pack", "Name Change Pack"], ["Name Change Entrance", "Cut Content Entrance"], player, + multiworld) + create_region_and_locations_lfod("Hard Cave", [], ["Hard Cave Wall Jump"], player, multiworld, 20) + create_region_and_locations_lfod("Hard Cave Wall Jump", ["Increased HP Pack"], [], player, multiworld, 130) + create_region_and_locations_lfod("Cut Content", conditional_location(has_item_shuffle, "Humble Indie Bindle"), [], player, multiworld, 200) + create_region_and_locations_lfod("Name Change", conditional_location(has_item_shuffle, "Box of Various Supplies"), ["Behind Rocks"], player, multiworld) + create_region_and_locations_lfod("Top Right", ["Season Pass", "High Definition Next Gen Pack"], ["Blizzard"], player, multiworld, 90) + create_region_and_locations_lfod("Season", ["Remove Ads Pack", "Not Exactly Noble"], ["Boss Door"], player, multiworld, 154) + region_final_boss = create_region_and_locations_lfod("Final Boss", ["Big Sword Pack", "Really Big Sword Pack", "Unfathomable Sword Pack"], [], player, multiworld) - Regdoubleleftcave = Region("Double Jump Total Left Cave", player, world) - Locdoubleleftcave_name = ["Top Hat Sheep"] - Regdoubleleftcave.locations += [DLCQuestLocation(player, loc_name, location_table[loc_name], Regdoubleleftcave) - for loc_name in Locdoubleleftcave_name] - add_coin_dlcquest(Regdoubleleftcave, 9, player) - world.regions.append(Regdoubleleftcave) + create_victory_event(region_final_boss, "Winning Freemium", "Victory Freemium", player) - Regdoubleleftroof = Region("Double Jump Total Left Roof", player, world) - Locdoubleleftroof_name = ["North West Ceiling Sheep"] - Regdoubleleftroof.locations += [DLCQuestLocation(player, loc_name, location_table[loc_name], Regdoubleleftroof) - for loc_name in Locdoubleleftroof_name] - add_coin_dlcquest(Regdoubleleftroof, 10, player) - world.regions.append(Regdoubleleftroof) + connect_entrances_lfod(multiworld, player) - Regdoubletree = Region("Double Jump Behind Tree", player, world) - Locdoubletree_name = ["Sexy Outfits Pack", "Double Jump Alcove Sheep", "Sexy Outfits Sheep"] - Regdoubletree.locations += [DLCQuestLocation(player, loc_name, location_table[loc_name], Regdoubletree) for - loc_name in - Locdoubletree_name] - Regdoubletree.exits = [Entrance(player, "True Double Jump", Regdoubletree)] - add_coin_dlcquest(Regdoubletree, 89, player) - world.regions.append(Regdoubletree) - Regtruedoublejump = Region("True Double Jump Behind Tree", player, world) - Loctruedoublejump_name = ["Double Jump Floating Sheep", "Cutscene Sheep"] - Regtruedoublejump.locations += [DLCQuestLocation(player, loc_name, location_table[loc_name], Regtruedoublejump) - for loc_name in Loctruedoublejump_name] - add_coin_dlcquest(Regtruedoublejump, 7, player) - world.regions.append(Regtruedoublejump) +def conditional_location(condition: bool, location: str) -> List[str]: + return conditional_locations(condition, [location]) - Regforest = Region("The Forest", player, world) - Locforest_name = ["Gun Pack", "Night Map Pack"] - Regforest.exits = [Entrance(player, "Behind Ogre", Regforest), - Entrance(player, "Forest Double Jump", Regforest)] - Regforest.locations += [DLCQuestLocation(player, loc_name, location_table[loc_name], Regforest) for loc_name in - Locforest_name] - add_coin_dlcquest(Regforest, 171, player) - world.regions.append(Regforest) - Regforestdoublejump = Region("The Forest whit double Jump", player, world) - Locforestdoublejump_name = ["The Zombie Pack", "Forest Low Sheep"] - Regforestdoublejump.exits = [Entrance(player, "Forest True Double Jump", Regforestdoublejump)] - Regforestdoublejump.locations += [ - DLCQuestLocation(player, loc_name, location_table[loc_name], Regforestdoublejump) for loc_name in - Locforestdoublejump_name] - add_coin_dlcquest(Regforestdoublejump, 76, player) - world.regions.append(Regforestdoublejump) +def conditional_locations(condition: bool, locations: List[str]) -> List[str]: + return locations if condition else [] - Regforesttruedoublejump = Region("The Forest whit double Jump Part 2", player, world) - Locforesttruedoublejump_name = ["Forest High Sheep"] - Regforesttruedoublejump.locations += [ - DLCQuestLocation(player, loc_name, location_table[loc_name], Regforesttruedoublejump) - for loc_name in Locforesttruedoublejump_name] - add_coin_dlcquest(Regforesttruedoublejump, 203, player) - world.regions.append(Regforesttruedoublejump) - Regfinalroom = Region("The Final Boss Room", player, world) - Locfinalroom_name = ["Finish the Fight Pack"] - Regfinalroom.locations += [DLCQuestLocation(player, loc_name, location_table[loc_name], Regfinalroom) for - loc_name in - Locfinalroom_name] - world.regions.append(Regfinalroom) +def create_region_and_locations_basic(region_name: str, locations: List[str], exits: List[str], player: int, multiworld: MultiWorld, + number_coins: int = 0) -> Region: + return create_region_and_locations(region_name, locations, exits, player, multiworld, number_coins, 0) - loc_win = DLCQuestLocation(player, "Winning Basic", None, world.get_region("The Final Boss Room", player)) - world.get_region("The Final Boss Room", player).locations.append(loc_win) - loc_win.place_locked_item(create_event(player, "Victory Basic")) - world.get_entrance("DLC Quest Basic", player).connect(world.get_region("Move Right", player)) +def create_region_and_locations_lfod(region_name: str, locations: List[str], exits: List[str], player: int, multiworld: MultiWorld, + number_coins: int = 0) -> Region: + return create_region_and_locations(region_name, locations, exits, player, multiworld, 0, number_coins) - world.get_entrance("Moving", player).connect(world.get_region("Movement Pack", player)) - world.get_entrance("Tree", player).connect(world.get_region("Behind Tree", player)) +def create_region_and_locations(region_name: str, locations: List[str], exits: List[str], player: int, multiworld: MultiWorld, + number_coins_basic: int, number_coins_lfod: int) -> Region: + region = Region(region_name, player, multiworld) + region.exits = [Entrance(player, exit_name, region) for exit_name in exits] + region.locations += [DLCQuestLocation(player, name, location_table[name], region) for name in locations] + if number_coins_basic > 0: + add_coin_dlcquest(region, number_coins_basic, player) + if number_coins_lfod > 0: + add_coin_lfod(region, number_coins_lfod, player) + multiworld.regions.append(region) + return region - world.get_entrance("Cloud", player).connect(world.get_region("Psychological Warfare", player)) - world.get_entrance("Cloud Double Jump", player).connect(world.get_region("Double Jump Total Left", player)) +def create_victory_event(region_victory: Region, event_name: str, item_name: str, player: int): + location_victory = DLCQuestLocation(player, event_name, None, region_victory) + region_victory.locations.append(location_victory) + location_victory.place_locked_item(create_event(player, item_name)) - world.get_entrance("Cave Tree", player).connect(world.get_region("Double Jump Total Left Cave", player)) - world.get_entrance("Cave Roof", player).connect(world.get_region("Double Jump Total Left Roof", player)) +def connect_entrances_basic(player, world): + world.get_entrance("DLC Quest Basic", player).connect(world.get_region("Move Right", player)) + world.get_entrance("Moving", player).connect(world.get_region("Movement Pack", player)) + world.get_entrance("Tree", player).connect(world.get_region("Behind Tree", player)) + world.get_entrance("Cloud", player).connect(world.get_region("Psychological Warfare", player)) + world.get_entrance("Cloud Double Jump", player).connect(world.get_region("Double Jump Total Left", player)) + world.get_entrance("Cave Tree", player).connect(world.get_region("Double Jump Total Left Cave", player)) + world.get_entrance("Cave Roof", player).connect(world.get_region("Double Jump Total Left Roof", player)) + world.get_entrance("Forest Entrance", player).connect(world.get_region("The Forest", player)) + world.get_entrance("Behind Tree Double Jump", player).connect(world.get_region("Double Jump Behind Tree", player)) + world.get_entrance("Behind Ogre", player).connect(world.get_region("The Final Boss Room", player)) + world.get_entrance("Forest Double Jump", player).connect(world.get_region("The Forest with double Jump", player)) + world.get_entrance("Forest True Double Jump", player).connect(world.get_region("The Forest with double Jump Part 2", player)) + world.get_entrance("True Double Jump", player).connect(world.get_region("True Double Jump Behind Tree", player)) - world.get_entrance("Forest Entrance", player).connect(world.get_region("The Forest", player)) - world.get_entrance("Behind Tree Double Jump", player).connect( - world.get_region("Double Jump Behind Tree", player)) +def connect_entrances_lfod(multiworld, player): + multiworld.get_entrance("Live Freemium or Die", player).connect(multiworld.get_region("Freemium Start", player)) + multiworld.get_entrance("Vines", player).connect(multiworld.get_region("Behind the Vines", player)) + multiworld.get_entrance("Wall Jump Entrance", player).connect(multiworld.get_region("Wall Jump", player)) + multiworld.get_entrance("Harmless Plants", player).connect(multiworld.get_region("Fake Ending", player)) + multiworld.get_entrance("Pickaxe Hard Cave", player).connect(multiworld.get_region("Hard Cave", player)) + multiworld.get_entrance("Hard Cave Wall Jump", player).connect(multiworld.get_region("Hard Cave Wall Jump", player)) + multiworld.get_entrance("Name Change Entrance", player).connect(multiworld.get_region("Name Change", player)) + multiworld.get_entrance("Cut Content Entrance", player).connect(multiworld.get_region("Cut Content", player)) + multiworld.get_entrance("Behind Rocks", player).connect(multiworld.get_region("Top Right", player)) + multiworld.get_entrance("Blizzard", player).connect(multiworld.get_region("Season", player)) + multiworld.get_entrance("Boss Door", player).connect(multiworld.get_region("Final Boss", player)) - world.get_entrance("Behind Ogre", player).connect(world.get_region("The Final Boss Room", player)) - world.get_entrance("Forest Double Jump", player).connect( - world.get_region("The Forest whit double Jump", player)) +def create_coinsanity_locations_dlc_quest(has_coinsanity: bool, coin_bundle_size: int, player: int, region_move_right: Region): + create_coinsanity_locations(has_coinsanity, coin_bundle_size, player, region_move_right, 825, "DLC Quest") - world.get_entrance("Forest True Double Jump", player).connect( - world.get_region("The Forest whit double Jump Part 2", player)) - world.get_entrance("True Double Jump", player).connect(world.get_region("True Double Jump Behind Tree", player)) +def create_coinsanity_locations_lfod(has_coinsanity: bool, coin_bundle_size: int, player: int, region_lfod_start: Region): + create_coinsanity_locations(has_coinsanity, coin_bundle_size, player, region_lfod_start, 889, "Live Freemium or Die") - if (World_Options.campaign == Options.Campaign.option_live_freemium_or_die or World_Options.campaign - == Options.Campaign.option_both): - Regfreemiumstart = Region("Freemium Start", player, world) - Locfreemiumstart_name = ["Particles Pack", "Day One Patch Pack", "Checkpoint Pack", "Incredibly Important Pack", - "Nice Try", "Story is Important", "I Get That Reference!"] - if World_Options.item_shuffle == Options.ItemShuffle.option_shuffled: - Locfreemiumstart_name += ["Wooden Sword"] - Regfreemiumstart.exits = [Entrance(player, "Vines", Regfreemiumstart)] - Regfreemiumstart.locations += [DLCQuestLocation(player, loc_name, location_table[loc_name], Regfreemiumstart) - for loc_name in - Locfreemiumstart_name] - add_coin_freemium(Regfreemiumstart, 50, player) - if World_Options.coinsanity == Options.CoinSanity.option_coin: - coin_bundle_needed = math.floor(889 / World_Options.coinbundlequantity) - for i in range(coin_bundle_needed): - item_coin_freemium = f"Live Freemium or Die: {World_Options.coinbundlequantity * (i + 1)} Coin" - Regfreemiumstart.locations += [ - DLCQuestLocation(player, item_coin_freemium, location_table[item_coin_freemium], - Regfreemiumstart)] - if 889 % World_Options.coinbundlequantity != 0: - Regfreemiumstart.locations += [ - DLCQuestLocation(player, "Live Freemium or Die: 889 Coin", - location_table["Live Freemium or Die: 889 Coin"], - Regfreemiumstart)] - world.regions.append(Regfreemiumstart) +def create_coinsanity_locations(has_coinsanity: bool, coin_bundle_size: int, player: int, region: Region, last_coin_number: int, campaign_prefix: str): + if not has_coinsanity: + return - Regbehindvine = Region("Behind the Vines", player, world) - Locbehindvine_name = ["Wall Jump Pack", "Health Bar Pack", "Parallax Pack"] - if World_Options.item_shuffle == Options.ItemShuffle.option_shuffled: - Locbehindvine_name += ["Pickaxe"] - Regbehindvine.exits = [Entrance(player, "Wall Jump Entrance", Regbehindvine)] - Regbehindvine.locations += [DLCQuestLocation(player, loc_name, location_table[loc_name], Regbehindvine) for - loc_name in Locbehindvine_name] - add_coin_freemium(Regbehindvine, 95, player) - world.regions.append(Regbehindvine) - - Regwalljump = Region("Wall Jump", player, world) - Locwalljump_name = ["Harmless Plants Pack", "Death of Comedy Pack", "Canadian Dialog Pack", "DLC NPC Pack"] - Regwalljump.exits = [Entrance(player, "Harmless Plants", Regwalljump), - Entrance(player, "Pickaxe Hard Cave", Regwalljump)] - Regwalljump.locations += [DLCQuestLocation(player, loc_name, location_table[loc_name], Regwalljump) for - loc_name in Locwalljump_name] - add_coin_freemium(Regwalljump, 150, player) - world.regions.append(Regwalljump) - - Regfakeending = Region("Fake Ending", player, world) - Locfakeending_name = ["Cut Content Pack", "Name Change Pack"] - Regfakeending.exits = [Entrance(player, "Name Change Entrance", Regfakeending), - Entrance(player, "Cut Content Entrance", Regfakeending)] - Regfakeending.locations += [DLCQuestLocation(player, loc_name, location_table[loc_name], Regfakeending) for - loc_name in Locfakeending_name] - world.regions.append(Regfakeending) - - Reghardcave = Region("Hard Cave", player, world) - add_coin_freemium(Reghardcave, 20, player) - Reghardcave.exits = [Entrance(player, "Hard Cave Wall Jump", Reghardcave)] - world.regions.append(Reghardcave) - - Reghardcavewalljump = Region("Hard Cave Wall Jump", player, world) - Lochardcavewalljump_name = ["Increased HP Pack"] - Reghardcavewalljump.locations += [ - DLCQuestLocation(player, loc_name, location_table[loc_name], Reghardcavewalljump) for - loc_name in Lochardcavewalljump_name] - add_coin_freemium(Reghardcavewalljump, 130, player) - world.regions.append(Reghardcavewalljump) - - Regcutcontent = Region("Cut Content", player, world) - Loccutcontent_name = [] - if World_Options.item_shuffle == Options.ItemShuffle.option_shuffled: - Loccutcontent_name += ["Humble Indie Bindle"] - Regcutcontent.locations += [DLCQuestLocation(player, loc_name, location_table[loc_name], Regcutcontent) for - loc_name in Loccutcontent_name] - add_coin_freemium(Regcutcontent, 200, player) - world.regions.append(Regcutcontent) - - Regnamechange = Region("Name Change", player, world) - Locnamechange_name = [] - if World_Options.item_shuffle == Options.ItemShuffle.option_shuffled: - Locnamechange_name += ["Box of Various Supplies"] - Regnamechange.exits = [Entrance(player, "Behind Rocks", Regnamechange)] - Regnamechange.locations += [DLCQuestLocation(player, loc_name, location_table[loc_name], Regnamechange) for - loc_name in Locnamechange_name] - world.regions.append(Regnamechange) - - Regtopright = Region("Top Right", player, world) - Loctopright_name = ["Season Pass", "High Definition Next Gen Pack"] - Regtopright.exits = [Entrance(player, "Blizzard", Regtopright)] - Regtopright.locations += [DLCQuestLocation(player, loc_name, location_table[loc_name], Regtopright) for - loc_name in Loctopright_name] - add_coin_freemium(Regtopright, 90, player) - world.regions.append(Regtopright) - - Regseason = Region("Season", player, world) - Locseason_name = ["Remove Ads Pack", "Not Exactly Noble"] - Regseason.exits = [Entrance(player, "Boss Door", Regseason)] - Regseason.locations += [DLCQuestLocation(player, loc_name, location_table[loc_name], Regseason) for - loc_name in Locseason_name] - add_coin_freemium(Regseason, 154, player) - world.regions.append(Regseason) - - Regfinalboss = Region("Final Boss", player, world) - Locfinalboss_name = ["Big Sword Pack", "Really Big Sword Pack", "Unfathomable Sword Pack"] - Regfinalboss.locations += [DLCQuestLocation(player, loc_name, location_table[loc_name], Regfinalboss) for - loc_name in Locfinalboss_name] - world.regions.append(Regfinalboss) - - loc_wining = DLCQuestLocation(player, "Winning Freemium", None, world.get_region("Final Boss", player)) - world.get_region("Final Boss", player).locations.append(loc_wining) - loc_wining.place_locked_item(create_event(player, "Victory Freemium")) - - world.get_entrance("Live Freemium or Die", player).connect(world.get_region("Freemium Start", player)) - - world.get_entrance("Vines", player).connect(world.get_region("Behind the Vines", player)) - - world.get_entrance("Wall Jump Entrance", player).connect(world.get_region("Wall Jump", player)) - - world.get_entrance("Harmless Plants", player).connect(world.get_region("Fake Ending", player)) - - world.get_entrance("Pickaxe Hard Cave", player).connect(world.get_region("Hard Cave", player)) - - world.get_entrance("Hard Cave Wall Jump", player).connect(world.get_region("Hard Cave Wall Jump", player)) - - world.get_entrance("Name Change Entrance", player).connect(world.get_region("Name Change", player)) - - world.get_entrance("Cut Content Entrance", player).connect(world.get_region("Cut Content", player)) - - world.get_entrance("Behind Rocks", player).connect(world.get_region("Top Right", player)) - - world.get_entrance("Blizzard", player).connect(world.get_region("Season", player)) - - world.get_entrance("Boss Door", player).connect(world.get_region("Final Boss", player)) + coin_bundle_needed = math.ceil(last_coin_number / coin_bundle_size) + for i in range(1, coin_bundle_needed + 1): + number_coins = min(last_coin_number, coin_bundle_size * i) + item_coin = f"{campaign_prefix}: {number_coins} Coin" + region.locations += [DLCQuestLocation(player, item_coin, location_table[item_coin], region)] diff --git a/worlds/dlcquest/test/TestItemShuffle.py b/worlds/dlcquest/test/TestItemShuffle.py new file mode 100644 index 0000000000..bfe999246a --- /dev/null +++ b/worlds/dlcquest/test/TestItemShuffle.py @@ -0,0 +1,130 @@ +from . import DLCQuestTestBase +from .. import Options + +sword = "Sword" +gun = "Gun" +wooden_sword = "Wooden Sword" +pickaxe = "Pickaxe" +humble_bindle = "Humble Indie Bindle" +box_supplies = "Box of Various Supplies" +items = [sword, gun, wooden_sword, pickaxe, humble_bindle, box_supplies] + +important_pack = "Incredibly Important Pack" + + +class TestItemShuffle(DLCQuestTestBase): + options = {Options.ItemShuffle.internal_name: Options.ItemShuffle.option_shuffled, + Options.Campaign.internal_name: Options.Campaign.option_both} + + def test_items_in_pool(self): + item_names = {item.name for item in self.multiworld.get_items()} + for item in items: + with self.subTest(f"{item}"): + self.assertIn(item, item_names) + + def test_item_locations_in_pool(self): + location_names = {location.name for location in self.multiworld.get_locations()} + for item_location in items: + with self.subTest(f"{item_location}"): + self.assertIn(item_location, location_names) + + def test_sword_location_has_correct_rules(self): + self.assertFalse(self.can_reach_location(sword)) + movement_pack = self.multiworld.create_item("Movement Pack", self.player) + self.collect(movement_pack) + self.assertFalse(self.can_reach_location(sword)) + time_pack = self.multiworld.create_item("Time is Money Pack", self.player) + self.collect(time_pack) + self.assertTrue(self.can_reach_location(sword)) + + def test_gun_location_has_correct_rules(self): + self.assertFalse(self.can_reach_location(gun)) + movement_pack = self.multiworld.create_item("Movement Pack", self.player) + self.collect(movement_pack) + self.assertFalse(self.can_reach_location(gun)) + sword_item = self.multiworld.create_item(sword, self.player) + self.collect(sword_item) + self.assertFalse(self.can_reach_location(gun)) + gun_pack = self.multiworld.create_item("Gun Pack", self.player) + self.collect(gun_pack) + self.assertTrue(self.can_reach_location(gun)) + + def test_wooden_sword_location_has_correct_rules(self): + self.assertFalse(self.can_reach_location(wooden_sword)) + important_pack_item = self.multiworld.create_item(important_pack, self.player) + self.collect(important_pack_item) + self.assertTrue(self.can_reach_location(wooden_sword)) + + def test_bindle_location_has_correct_rules(self): + self.assertFalse(self.can_reach_location(humble_bindle)) + wooden_sword_item = self.multiworld.create_item(wooden_sword, self.player) + self.collect(wooden_sword_item) + self.assertFalse(self.can_reach_location(humble_bindle)) + plants_pack = self.multiworld.create_item("Harmless Plants Pack", self.player) + self.collect(plants_pack) + self.assertFalse(self.can_reach_location(humble_bindle)) + wall_jump_pack = self.multiworld.create_item("Wall Jump Pack", self.player) + self.collect(wall_jump_pack) + self.assertFalse(self.can_reach_location(humble_bindle)) + name_change_pack = self.multiworld.create_item("Name Change Pack", self.player) + self.collect(name_change_pack) + self.assertFalse(self.can_reach_location(humble_bindle)) + cut_content_pack = self.multiworld.create_item("Cut Content Pack", self.player) + self.collect(cut_content_pack) + self.assertFalse(self.can_reach_location(humble_bindle)) + box_supplies_item = self.multiworld.create_item(box_supplies, self.player) + self.collect(box_supplies_item) + self.assertTrue(self.can_reach_location(humble_bindle)) + + def test_box_supplies_location_has_correct_rules(self): + self.assertFalse(self.can_reach_location(box_supplies)) + wooden_sword_item = self.multiworld.create_item(wooden_sword, self.player) + self.collect(wooden_sword_item) + self.assertFalse(self.can_reach_location(box_supplies)) + plants_pack = self.multiworld.create_item("Harmless Plants Pack", self.player) + self.collect(plants_pack) + self.assertFalse(self.can_reach_location(box_supplies)) + wall_jump_pack = self.multiworld.create_item("Wall Jump Pack", self.player) + self.collect(wall_jump_pack) + self.assertFalse(self.can_reach_location(box_supplies)) + name_change_pack = self.multiworld.create_item("Name Change Pack", self.player) + self.collect(name_change_pack) + self.assertFalse(self.can_reach_location(box_supplies)) + cut_content_pack = self.multiworld.create_item("Cut Content Pack", self.player) + self.collect(cut_content_pack) + self.assertTrue(self.can_reach_location(box_supplies)) + + def test_pickaxe_location_has_correct_rules(self): + self.assertFalse(self.can_reach_location(pickaxe)) + wooden_sword_item = self.multiworld.create_item(wooden_sword, self.player) + self.collect(wooden_sword_item) + self.assertFalse(self.can_reach_location(pickaxe)) + plants_pack = self.multiworld.create_item("Harmless Plants Pack", self.player) + self.collect(plants_pack) + self.assertFalse(self.can_reach_location(pickaxe)) + wall_jump_pack = self.multiworld.create_item("Wall Jump Pack", self.player) + self.collect(wall_jump_pack) + self.assertFalse(self.can_reach_location(pickaxe)) + name_change_pack = self.multiworld.create_item("Name Change Pack", self.player) + self.collect(name_change_pack) + self.assertFalse(self.can_reach_location(pickaxe)) + bindle_item = self.multiworld.create_item("Humble Indie Bindle", self.player) + self.collect(bindle_item) + self.assertTrue(self.can_reach_location(pickaxe)) + + +class TestNoItemShuffle(DLCQuestTestBase): + options = {Options.ItemShuffle.internal_name: Options.ItemShuffle.option_disabled, + Options.Campaign.internal_name: Options.Campaign.option_both} + + def test_items_not_in_pool(self): + item_names = {item.name for item in self.multiworld.get_items()} + for item in items: + with self.subTest(f"{item}"): + self.assertNotIn(item, item_names) + + def test_item_locations_not_in_pool(self): + location_names = {location.name for location in self.multiworld.get_locations()} + for item_location in items: + with self.subTest(f"{item_location}"): + self.assertNotIn(item_location, location_names) \ No newline at end of file diff --git a/worlds/dlcquest/test/TestOptionsLong.py b/worlds/dlcquest/test/TestOptionsLong.py new file mode 100644 index 0000000000..d0a5c0ed7d --- /dev/null +++ b/worlds/dlcquest/test/TestOptionsLong.py @@ -0,0 +1,87 @@ +from typing import Dict + +from BaseClasses import MultiWorld +from Options import SpecialRange +from .option_names import options_to_include +from .checks.world_checks import assert_can_win, assert_same_number_items_locations +from . import DLCQuestTestBase, setup_dlc_quest_solo_multiworld +from ... import AutoWorldRegister + + +def basic_checks(tester: DLCQuestTestBase, multiworld: MultiWorld): + assert_can_win(tester, multiworld) + assert_same_number_items_locations(tester, multiworld) + + +def get_option_choices(option) -> Dict[str, int]: + if issubclass(option, SpecialRange): + return option.special_range_names + elif option.options: + return option.options + return {} + + +class TestGenerateDynamicOptions(DLCQuestTestBase): + def test_given_option_pair_when_generate_then_basic_checks(self): + num_options = len(options_to_include) + for option1_index in range(0, num_options): + for option2_index in range(option1_index + 1, num_options): + option1 = options_to_include[option1_index] + option2 = options_to_include[option2_index] + option1_choices = get_option_choices(option1) + option2_choices = get_option_choices(option2) + for key1 in option1_choices: + for key2 in option2_choices: + with self.subTest(f"{option1.internal_name}: {key1}, {option2.internal_name}: {key2}"): + choices = {option1.internal_name: option1_choices[key1], + option2.internal_name: option2_choices[key2]} + multiworld = setup_dlc_quest_solo_multiworld(choices) + basic_checks(self, multiworld) + + def test_given_option_truple_when_generate_then_basic_checks(self): + num_options = len(options_to_include) + for option1_index in range(0, num_options): + for option2_index in range(option1_index + 1, num_options): + for option3_index in range(option2_index + 1, num_options): + option1 = options_to_include[option1_index] + option2 = options_to_include[option2_index] + option3 = options_to_include[option3_index] + option1_choices = get_option_choices(option1) + option2_choices = get_option_choices(option2) + option3_choices = get_option_choices(option3) + for key1 in option1_choices: + for key2 in option2_choices: + for key3 in option3_choices: + with self.subTest(f"{option1.internal_name}: {key1}, {option2.internal_name}: {key2}, {option3.internal_name}: {key3}"): + choices = {option1.internal_name: option1_choices[key1], + option2.internal_name: option2_choices[key2], + option3.internal_name: option3_choices[key3]} + multiworld = setup_dlc_quest_solo_multiworld(choices) + basic_checks(self, multiworld) + + def test_given_option_quartet_when_generate_then_basic_checks(self): + num_options = len(options_to_include) + for option1_index in range(0, num_options): + for option2_index in range(option1_index + 1, num_options): + for option3_index in range(option2_index + 1, num_options): + for option4_index in range(option3_index + 1, num_options): + option1 = options_to_include[option1_index] + option2 = options_to_include[option2_index] + option3 = options_to_include[option3_index] + option4 = options_to_include[option4_index] + option1_choices = get_option_choices(option1) + option2_choices = get_option_choices(option2) + option3_choices = get_option_choices(option3) + option4_choices = get_option_choices(option4) + for key1 in option1_choices: + for key2 in option2_choices: + for key3 in option3_choices: + for key4 in option4_choices: + with self.subTest( + f"{option1.internal_name}: {key1}, {option2.internal_name}: {key2}, {option3.internal_name}: {key3}, {option4.internal_name}: {key4}"): + choices = {option1.internal_name: option1_choices[key1], + option2.internal_name: option2_choices[key2], + option3.internal_name: option3_choices[key3], + option4.internal_name: option4_choices[key4]} + multiworld = setup_dlc_quest_solo_multiworld(choices) + basic_checks(self, multiworld) diff --git a/worlds/dlcquest/test/__init__.py b/worlds/dlcquest/test/__init__.py new file mode 100644 index 0000000000..e998bd8a5e --- /dev/null +++ b/worlds/dlcquest/test/__init__.py @@ -0,0 +1,53 @@ +from typing import ClassVar + +from typing import Dict, FrozenSet, Tuple, Any +from argparse import Namespace + +from BaseClasses import MultiWorld +from test.TestBase import WorldTestBase +from .. import DLCqworld +from test.general import gen_steps, setup_solo_multiworld as setup_base_solo_multiworld +from worlds.AutoWorld import call_all + + +class DLCQuestTestBase(WorldTestBase): + game = "DLCQuest" + world: DLCqworld + player: ClassVar[int] = 1 + + def world_setup(self, *args, **kwargs): + super().world_setup(*args, **kwargs) + if self.constructed: + self.world = self.multiworld.worlds[self.player] # noqa + + @property + def run_default_tests(self) -> bool: + # world_setup is overridden, so it'd always run default tests when importing DLCQuestTestBase + is_not_dlc_test = type(self) is not DLCQuestTestBase + should_run_default_tests = is_not_dlc_test and super().run_default_tests + return should_run_default_tests + + +def setup_dlc_quest_solo_multiworld(test_options=None, seed=None, _cache: Dict[FrozenSet[Tuple[str, Any]], MultiWorld] = {}) -> MultiWorld: #noqa + if test_options is None: + test_options = {} + + # Yes I reuse the worlds generated between tests, its speeds the execution by a couple seconds + frozen_options = frozenset(test_options.items()).union({seed}) + if frozen_options in _cache: + return _cache[frozen_options] + + multiworld = setup_base_solo_multiworld(DLCqworld, ()) + multiworld.set_seed(seed) + # print(f"Seed: {multiworld.seed}") # Uncomment to print the seed for every test + args = Namespace() + for name, option in DLCqworld.options_dataclass.type_hints.items(): + value = option(test_options[name]) if name in test_options else option.from_any(option.default) + setattr(args, name, {1: value}) + multiworld.set_options(args) + for step in gen_steps: + call_all(multiworld, step) + + _cache[frozen_options] = multiworld + + return multiworld diff --git a/worlds/dlcquest/test/checks/__init__.py b/worlds/dlcquest/test/checks/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/worlds/dlcquest/test/checks/world_checks.py b/worlds/dlcquest/test/checks/world_checks.py new file mode 100644 index 0000000000..a97093d620 --- /dev/null +++ b/worlds/dlcquest/test/checks/world_checks.py @@ -0,0 +1,42 @@ +from typing import List + +from BaseClasses import MultiWorld, ItemClassification +from .. import DLCQuestTestBase +from ... import Options + + +def get_all_item_names(multiworld: MultiWorld) -> List[str]: + return [item.name for item in multiworld.itempool] + + +def get_all_location_names(multiworld: MultiWorld) -> List[str]: + return [location.name for location in multiworld.get_locations() if not location.event] + + +def assert_victory_exists(tester: DLCQuestTestBase, multiworld: MultiWorld): + campaign = multiworld.campaign[1] + all_items = [item.name for item in multiworld.get_items()] + if campaign == Options.Campaign.option_basic or campaign == Options.Campaign.option_both: + tester.assertIn("Victory Basic", all_items) + if campaign == Options.Campaign.option_live_freemium_or_die or campaign == Options.Campaign.option_both: + tester.assertIn("Victory Freemium", all_items) + + +def collect_all_then_assert_can_win(tester: DLCQuestTestBase, multiworld: MultiWorld): + for item in multiworld.get_items(): + multiworld.state.collect(item) + campaign = multiworld.campaign[1] + if campaign == Options.Campaign.option_basic or campaign == Options.Campaign.option_both: + tester.assertTrue(multiworld.find_item("Victory Basic", 1).can_reach(multiworld.state)) + if campaign == Options.Campaign.option_live_freemium_or_die or campaign == Options.Campaign.option_both: + tester.assertTrue(multiworld.find_item("Victory Freemium", 1).can_reach(multiworld.state)) + + +def assert_can_win(tester: DLCQuestTestBase, multiworld: MultiWorld): + assert_victory_exists(tester, multiworld) + collect_all_then_assert_can_win(tester, multiworld) + + +def assert_same_number_items_locations(tester: DLCQuestTestBase, multiworld: MultiWorld): + non_event_locations = [location for location in multiworld.get_locations() if not location.event] + tester.assertEqual(len(multiworld.itempool), len(non_event_locations)) \ No newline at end of file diff --git a/worlds/dlcquest/test/option_names.py b/worlds/dlcquest/test/option_names.py new file mode 100644 index 0000000000..4a4b46e906 --- /dev/null +++ b/worlds/dlcquest/test/option_names.py @@ -0,0 +1,5 @@ +from .. import DLCqworld + +options_to_exclude = ["progression_balancing", "accessibility", "start_inventory", "start_hints", "death_link"] +options_to_include = [option for option_name, option in DLCqworld.options_dataclass.type_hints.items() + if option_name not in options_to_exclude] From 7aab9d44394b005dc1c0ee6e83d25f4bc9326ecb Mon Sep 17 00:00:00 2001 From: Alchav <59858495+Alchav@users.noreply.github.com> Date: Wed, 18 Oct 2023 15:55:03 -0400 Subject: [PATCH 11/54] =?UTF-8?q?Docs:=20Recommend=20Bizhawk=20Version=202?= =?UTF-8?q?.9.1=20for=20Pok=C3=A9mon=20R/B=20(#2320)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- worlds/pokemon_rb/docs/setup_en.md | 6 +++--- worlds/pokemon_rb/docs/setup_es.md | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/worlds/pokemon_rb/docs/setup_en.md b/worlds/pokemon_rb/docs/setup_en.md index 488f3fdc07..7ba9b3aa09 100644 --- a/worlds/pokemon_rb/docs/setup_en.md +++ b/worlds/pokemon_rb/docs/setup_en.md @@ -7,7 +7,7 @@ As we are using BizHawk, this guide is only applicable to Windows and Linux syst ## Required Software - BizHawk: [BizHawk Releases from TASVideos](https://tasvideos.org/BizHawk/ReleaseHistory) - - Version 2.3.1 and later are supported. Version 2.7 is recommended for stability. + - Version 2.3.1 and later are supported. Version 2.9.1 is recommended. - Detailed installation instructions for BizHawk can be found at the above link. - Windows users must run the prereq installer first, which can also be found at the above link. - The built-in Archipelago client, which can be installed [here](https://github.com/ArchipelagoMW/Archipelago/releases) @@ -23,7 +23,7 @@ As we are using BizHawk, this guide is only applicable to Windows and Linux syst Once BizHawk has been installed, open EmuHawk and change the following settings: -- (≤ 2.8) Go to Config > Customize. Switch to the Advanced tab, then switch the Lua Core from "NLua+KopiLua" to +- (If using 2.8 or earlier) Go to Config > Customize. Switch to the Advanced tab, then switch the Lua Core from "NLua+KopiLua" to "Lua+LuaInterface". Then restart EmuHawk. This is required for the Lua script to function correctly. **NOTE: Even if "Lua+LuaInterface" is already selected, toggle between the two options and reselect it. Fresh installs** **of newer versions of EmuHawk have a tendency to show "Lua+LuaInterface" as the default selected option but still load** @@ -57,7 +57,7 @@ For `trainer_name` and `rival_name` the following regular characters are allowed * `‘’“”·… ABCDEFGHIJKLMNOPQRSTUVWXYZ():;[]abcdefghijklmnopqrstuvwxyzé'-?!.♂$×/,♀0123456789` -And the following special characters (these each take up one character): +And the following special characters (these each count as one character): * `<'d>` * `<'l>` * `<'t>` diff --git a/worlds/pokemon_rb/docs/setup_es.md b/worlds/pokemon_rb/docs/setup_es.md index 2a943da72f..a6a6aa6ce7 100644 --- a/worlds/pokemon_rb/docs/setup_es.md +++ b/worlds/pokemon_rb/docs/setup_es.md @@ -7,7 +7,7 @@ Al usar BizHawk, esta guía solo es aplicable en los sistemas de Windows y Linux ## Software Requerido - BizHawk: [BizHawk Releases en TASVideos](https://tasvideos.org/BizHawk/ReleaseHistory) - - La versión 2.3.1 y posteriores son soportadas. Se recomienda la versión 2.7 para estabilidad. + - La versión 2.3.1 y posteriores son soportadas. Se recomienda la versión 2.9.1. - Instrucciones de instalación detalladas para BizHawk se pueden encontrar en el enlace de arriba. - Los usuarios de Windows deben ejecutar el instalador de prerrequisitos (prereq installer) primero, que también se encuentra en el enlace de arriba. From 45e69f3d268a56badb3868e06e0ad303ef801740 Mon Sep 17 00:00:00 2001 From: Zach Parks Date: Wed, 18 Oct 2023 15:11:25 -0500 Subject: [PATCH 12/54] Docs: Triage role expectations documentation. (#2325) Co-authored-by: Scipio Wright --- docs/triage role expectations.md | 100 +++++++++++++++++++++++++++++++ 1 file changed, 100 insertions(+) create mode 100644 docs/triage role expectations.md diff --git a/docs/triage role expectations.md b/docs/triage role expectations.md new file mode 100644 index 0000000000..5b4cab2275 --- /dev/null +++ b/docs/triage role expectations.md @@ -0,0 +1,100 @@ +# Triage Role Expectations + +Users with Triage-level access are selected contributors who can and wish to proactively label/triage issues and pull +requests without being granted write access to the Archipelago repository. + +Triage users are not necessarily official members of the Archipelago organization, for the list of core maintainers, +please reference [ArchipelagoMW Members](https://github.com/orgs/ArchipelagoMW/people) page. + +## Access Permissions + +Triage users have the following permissions: + +* Apply/dismiss labels on all issues and pull requests. +* Close, reopen, and assign all issues and pull requests. +* Mark issues and pull requests as duplicate. +* Request pull request reviews from repository members. +* Hide comments in issues or pull requests from public view. + * Hidden comments are not deleted and can be reversed by another triage user or repository member with write access. +* And all other standard permissions granted to regular GitHub users. + +For more details on permissions granted by the Triage role, see +[GitHub's Role Documentation](https://docs.github.com/en/organizations/managing-user-access-to-your-organizations-repositories/managing-repository-roles/repository-roles-for-an-organization). + +## Expectations + +Users with triage-level permissions have no expectation to review code, but, if desired, to review pull requests/issues +and apply the relevant labels and ping/request reviews from any relevant [code owners](./CODEOWNERS) for review. Triage +users are also expected not to close others' issues or pull requests without strong reason to do so (with exception of +`meta: invalid` or `meta: duplicate` scenarios, which are listed below). When in doubt, defer to a core maintainer. + +Triage users are not "moderators" for others' issues or pull requests. However, they may voice their opinions/feedback +on issues or pull requests, just the same as any other GitHub user contributing to Archipelago. + +## Labeling + +As of the time of writing this document, there are 15 distinct labels that can be applied to issues and pull requests. + +### Affects + +These labels notate if certain issues or pull requests affect critical aspects of Archipelago that may require specific +review. More than one of these labels can be used on a issue or pull request, if relevant. + +* `affects: core` is to be applied to issues/PRs that may affect core Archipelago functionality and should be reviewed +with additional scrutiny. + * Core is defined as any files not contained in the `WebHostLib` directory or individual world implementations + directories inside the `worlds` directory, not including `worlds/generic`. +* `affects: webhost` is to be applied to issues/PRs that may affect the core WebHost portion of Archipelago. In +general, this is anything being modified inside the `WebHostLib` directory or `WebHost.py` file. +* `affects: release/blocker` is to be applied for any issues/PRs that may either negatively impact (issues) or propose +to resolve critical issues (pull requests) that affect the current or next official release of Archipelago and should be +given top priority for review. + +### Is + +These labels notate what kinds of changes are being made or proposed in issues or pull requests. More than one of these +labels can be used on a issue or pull request, if relevant, but at least one of these labels should be applied to every +pull request and issue. + +* `is: bug/fix` is to be applied to issues/PRs that report or resolve an issue in core, web, or individual world +implementations. +* `is: documentation` is to be applied to issues/PRs that relate to adding, updating, or removing documentation in +core, web, or individual world implementations without modifying actual code. +* `is: enhancement` is to be applied to issues/PRs that relate to adding, modifying, or removing functionality in +core, web, or individual world implementations. +* `is: refactor/cleanup` is to be applied to issues/PRs that relate to reorganizing existing code to improve +readability or performance without adding, modifying, or removing functionality or fixing known regressions. +* `is: maintenance` is to be applied to issues/PRs that don't modify logic, refactor existing code, change features. +This is typically reserved for pull requests that need to update dependencies or increment version numbers without +resolving existing issues. +* `is: new game` is to be applied to any pull requests that introduce a new game for the first time to the `worlds` +directory. + * Issues should not be opened and classified with `is: new game`, and instead should be directed to the + #future-game-design channel in Archipelago for opening suggestions. If they are opened, they should be labeled + with `meta: invalid` and closed. + * Pull requests for new games should only have this label, as enhancement, documentation, bug/fix, refactor, and + possibly maintenance is implied. + +### Meta + +These labels allow additional quick meta information for contributors or reviewers for issues and pull requests. They +have specific situations where they should be applied. + +* `meta: duplicate` is to be applied to any issues/PRs that are duplicate of another issue/PR that was already opened. + * These should be immediately closed after leaving a comment, directing to the original issue or pull request. +* `meta: invalid` is to be applied to any issues/PRs that do not relate to Archipelago or are inappropriate for +discussion on GitHub. + * These should be immediately closed afterwards. +* `meta: help wanted` is to be applied to any issues/PRs that require additional attention for whatever reason. + * These should include a comment describing what kind of help is requested when the label is added. + * Some common reasons include, but are not limited to: Breaking API changes that require developer input/testing or + pull requests with large line changes that need additional reviewers to be reviewed effectively. + * This label may require some programming experience and familiarity with Archipelago source to determine if + requesting additional attention for help is warranted. +* `meta: good first issue` is to be applied to any issues that may be a good starting ground for new contributors to try +and tackle. + * This label may require some programming experience and familiarity with Archipelago source to determine if an + issue is a "good first issue". +* `meta: wontfix` is to be applied for any issues/PRs that are opened that will not be actioned because it's out of +scope or determined to not be an issue. + * This should be reserved for use by a world's code owner(s) on their relevant world or by core maintainers. From e8a48da315c3e9e5c769e23cdb92b18394bf388c Mon Sep 17 00:00:00 2001 From: Aaron Wagener Date: Wed, 18 Oct 2023 16:04:12 -0500 Subject: [PATCH 13/54] SM: fix missing option import (#2326) --- worlds/sm/__init__.py | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/worlds/sm/__init__.py b/worlds/sm/__init__.py index 9d6f28607e..f208e600b9 100644 --- a/worlds/sm/__init__.py +++ b/worlds/sm/__init__.py @@ -1,18 +1,17 @@ from __future__ import annotations -import logging -import copy -import os -import threading import base64 -import settings +import copy +import logging +import threading import typing from typing import Any, Dict, Iterable, List, Set, TextIO, TypedDict -from BaseClasses import Region, Entrance, Location, MultiWorld, Item, ItemClassification, CollectionState, Tutorial -from Fill import fill_restrictive -from worlds.AutoWorld import World, AutoLogicRegister, WebWorld -from worlds.generic.Rules import set_rule, add_rule, add_item_rule +import settings +from BaseClasses import CollectionState, Entrance, Item, ItemClassification, Location, MultiWorld, Region, Tutorial +from Options import Accessibility +from worlds.AutoWorld import AutoLogicRegister, WebWorld, World +from worlds.generic.Rules import add_rule, set_rule logger = logging.getLogger("Super Metroid") From 1c7c83c69e97a19064a3bf1978be882662e8aee1 Mon Sep 17 00:00:00 2001 From: PsyMarth Date: Wed, 18 Oct 2023 14:53:54 -0700 Subject: [PATCH 14/54] OoT: Update Utils.py (#2310) Removed optional maxsize parameter, setting it to the default of 128. --- worlds/oot/Utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worlds/oot/Utils.py b/worlds/oot/Utils.py index c2444cd1fe..9faffbdedd 100644 --- a/worlds/oot/Utils.py +++ b/worlds/oot/Utils.py @@ -11,7 +11,7 @@ def data_path(*args): return os.path.join(os.path.dirname(__file__), 'data', *args) -@lru_cache(maxsize=13) # Cache Overworld.json and the 12 dungeons +@lru_cache def read_json(file_path): json_string = "" with io.open(file_path, 'r') as file: From 38c9ee146d32e5930f1ac8fa2817616f51f01128 Mon Sep 17 00:00:00 2001 From: Natalie Weizenbaum Date: Wed, 18 Oct 2023 15:26:52 -0700 Subject: [PATCH 15/54] WebHost: Refactor weighted-settings.js (#2318) * Refactor weighted-settings.js This moves most of the infrastructure into two classes: * WeightedSettings covers the settings page as a whole. It tracks the user's current settings in local storage as well as the game data from the server so they don't need to be manually passed around from function to function. * GameSettings covers the settings for a single game, and provides a view of the current settings and the game data just for that game. * Fix item count updating --- WebHostLib/static/assets/weighted-settings.js | 2042 +++++++++-------- 1 file changed, 1037 insertions(+), 1005 deletions(-) diff --git a/WebHostLib/static/assets/weighted-settings.js b/WebHostLib/static/assets/weighted-settings.js index fb7d3a349b..2cd61d2e6e 100644 --- a/WebHostLib/static/assets/weighted-settings.js +++ b/WebHostLib/static/assets/weighted-settings.js @@ -1,14 +1,14 @@ window.addEventListener('load', () => { - fetchSettingData().then((results) => { + fetchSettingData().then((data) => { let settingHash = localStorage.getItem('weighted-settings-hash'); if (!settingHash) { // If no hash data has been set before, set it now - settingHash = md5(JSON.stringify(results)); + settingHash = md5(JSON.stringify(data)); localStorage.setItem('weighted-settings-hash', settingHash); localStorage.removeItem('weighted-settings'); } - if (settingHash !== md5(JSON.stringify(results))) { + if (settingHash !== md5(JSON.stringify(data))) { const userMessage = document.getElementById('user-message'); userMessage.innerText = "Your settings are out of date! Click here to update them! Be aware this will reset " + "them all to default."; @@ -17,23 +17,22 @@ window.addEventListener('load', () => { } // Page setup - createDefaultSettings(results); - buildUI(results); - updateVisibleGames(); + const settings = new WeightedSettings(data); + settings.buildUI(); + settings.updateVisibleGames(); adjustHeaderWidth(); // Event listeners - document.getElementById('export-settings').addEventListener('click', () => exportSettings()); - document.getElementById('generate-race').addEventListener('click', () => generateGame(true)); - document.getElementById('generate-game').addEventListener('click', () => generateGame()); + document.getElementById('export-settings').addEventListener('click', () => settings.export()); + document.getElementById('generate-race').addEventListener('click', () => settings.generateGame(true)); + document.getElementById('generate-game').addEventListener('click', () => settings.generateGame()); // Name input field - const weightedSettings = JSON.parse(localStorage.getItem('weighted-settings')); const nameInput = document.getElementById('player-name'); nameInput.setAttribute('data-type', 'data'); nameInput.setAttribute('data-setting', 'name'); - nameInput.addEventListener('keyup', updateBaseSetting); - nameInput.value = weightedSettings.name; + nameInput.addEventListener('keyup', (evt) => settings.updateBaseSetting(evt)); + nameInput.value = settings.current.name; }); }); @@ -50,48 +49,65 @@ const fetchSettingData = () => new Promise((resolve, reject) => { }); }); -const createDefaultSettings = (settingData) => { - if (!localStorage.getItem('weighted-settings')) { - const newSettings = {}; +/// The weighted settings across all games. +class WeightedSettings { + // The data from the server describing the types of settings available for + // each game, as a JSON-safe blob. + data; + + // The settings chosen by the user as they'd appear in the YAML file, stored + // to and retrieved from local storage. + current; + + // A record mapping game names to the associated GameSettings. + games; + + constructor(data) { + this.data = data; + this.current = JSON.parse(localStorage.getItem('weighted-settings')); + this.games = Object.keys(this.data.games).map((game) => new GameSettings(this, game)); + if (this.current) { return; } + + this.current = {}; // Transfer base options directly - for (let baseOption of Object.keys(settingData.baseOptions)){ - newSettings[baseOption] = settingData.baseOptions[baseOption]; + for (let baseOption of Object.keys(this.data.baseOptions)){ + this.current[baseOption] = this.data.baseOptions[baseOption]; } // Set options per game - for (let game of Object.keys(settingData.games)) { + for (let game of Object.keys(this.data.games)) { // Initialize game object - newSettings[game] = {}; + this.current[game] = {}; // Transfer game settings - for (let gameSetting of Object.keys(settingData.games[game].gameSettings)){ - newSettings[game][gameSetting] = {}; + for (let gameSetting of Object.keys(this.data.games[game].gameSettings)){ + this.current[game][gameSetting] = {}; - const setting = settingData.games[game].gameSettings[gameSetting]; + const setting = this.data.games[game].gameSettings[gameSetting]; switch(setting.type){ case 'select': setting.options.forEach((option) => { - newSettings[game][gameSetting][option.value] = + this.current[game][gameSetting][option.value] = (setting.hasOwnProperty('defaultValue') && setting.defaultValue === option.value) ? 25 : 0; }); break; case 'range': case 'special_range': - newSettings[game][gameSetting]['random'] = 0; - newSettings[game][gameSetting]['random-low'] = 0; - newSettings[game][gameSetting]['random-high'] = 0; + this.current[game][gameSetting]['random'] = 0; + this.current[game][gameSetting]['random-low'] = 0; + this.current[game][gameSetting]['random-high'] = 0; if (setting.hasOwnProperty('defaultValue')) { - newSettings[game][gameSetting][setting.defaultValue] = 25; + this.current[game][gameSetting][setting.defaultValue] = 25; } else { - newSettings[game][gameSetting][setting.min] = 25; + this.current[game][gameSetting][setting.min] = 25; } break; case 'items-list': case 'locations-list': case 'custom-list': - newSettings[game][gameSetting] = setting.defaultValue; + this.current[game][gameSetting] = setting.defaultValue; break; default: @@ -99,33 +115,301 @@ const createDefaultSettings = (settingData) => { } } - newSettings[game].start_inventory = {}; - newSettings[game].exclude_locations = []; - newSettings[game].priority_locations = []; - newSettings[game].local_items = []; - newSettings[game].non_local_items = []; - newSettings[game].start_hints = []; - newSettings[game].start_location_hints = []; + this.current[game].start_inventory = {}; + this.current[game].exclude_locations = []; + this.current[game].priority_locations = []; + this.current[game].local_items = []; + this.current[game].non_local_items = []; + this.current[game].start_hints = []; + this.current[game].start_location_hints = []; } - localStorage.setItem('weighted-settings', JSON.stringify(newSettings)); + this.save(); } -}; -const buildUI = (settingData) => { - // Build the game-choice div - buildGameChoice(settingData.games); + // Saves the current settings to local storage. + save() { + localStorage.setItem('weighted-settings', JSON.stringify(this.current)); + } - const gamesWrapper = document.getElementById('games-wrapper'); - Object.keys(settingData.games).forEach((game) => { + buildUI() { + // Build the game-choice div + this.#buildGameChoice(); + + const gamesWrapper = document.getElementById('games-wrapper'); + this.games.forEach((game) => { + gamesWrapper.appendChild(game.buildUI()); + }); + } + + #buildGameChoice() { + const gameChoiceDiv = document.getElementById('game-choice'); + const h2 = document.createElement('h2'); + h2.innerText = 'Game Select'; + gameChoiceDiv.appendChild(h2); + + const gameSelectDescription = document.createElement('p'); + gameSelectDescription.classList.add('setting-description'); + gameSelectDescription.innerText = 'Choose which games you might be required to play.'; + gameChoiceDiv.appendChild(gameSelectDescription); + + const hintText = document.createElement('p'); + hintText.classList.add('hint-text'); + hintText.innerText = 'If a game\'s value is greater than zero, you can click it\'s name to jump ' + + 'to that section.' + gameChoiceDiv.appendChild(hintText); + + // Build the game choice table + const table = document.createElement('table'); + const tbody = document.createElement('tbody'); + + Object.keys(this.data.games).forEach((game) => { + const tr = document.createElement('tr'); + const tdLeft = document.createElement('td'); + tdLeft.classList.add('td-left'); + const span = document.createElement('span'); + span.innerText = game; + span.setAttribute('id', `${game}-game-option`) + tdLeft.appendChild(span); + tr.appendChild(tdLeft); + + const tdMiddle = document.createElement('td'); + tdMiddle.classList.add('td-middle'); + const range = document.createElement('input'); + range.setAttribute('type', 'range'); + range.setAttribute('min', 0); + range.setAttribute('max', 50); + range.setAttribute('data-type', 'weight'); + range.setAttribute('data-setting', 'game'); + range.setAttribute('data-option', game); + range.value = this.current.game[game]; + range.addEventListener('change', (evt) => { + this.updateBaseSetting(evt); + this.updateVisibleGames(); // Show or hide games based on the new settings + }); + tdMiddle.appendChild(range); + tr.appendChild(tdMiddle); + + const tdRight = document.createElement('td'); + tdRight.setAttribute('id', `game-${game}`) + tdRight.classList.add('td-right'); + tdRight.innerText = range.value; + tr.appendChild(tdRight); + tbody.appendChild(tr); + }); + + table.appendChild(tbody); + gameChoiceDiv.appendChild(table); + } + + // Verifies that `this.settings` meets all the requirements for world + // generation, normalizes it for serialization, and returns the result. + #validateSettings() { + const settings = structuredClone(this.current); + const userMessage = document.getElementById('user-message'); + let errorMessage = null; + + // User must choose a name for their file + if (!settings.name || settings.name.trim().length === 0 || settings.name.toLowerCase().trim() === 'player') { + userMessage.innerText = 'You forgot to set your player name at the top of the page!'; + userMessage.classList.add('visible'); + userMessage.scrollIntoView({ + behavior: 'smooth', + block: 'start', + }); + return; + } + + // Clean up the settings output + Object.keys(settings.game).forEach((game) => { + // Remove any disabled games + if (settings.game[game] === 0) { + delete settings.game[game]; + delete settings[game]; + return; + } + + Object.keys(settings[game]).forEach((setting) => { + // Remove any disabled options + Object.keys(settings[game][setting]).forEach((option) => { + if (settings[game][setting][option] === 0) { + delete settings[game][setting][option]; + } + }); + + if ( + Object.keys(settings[game][setting]).length === 0 && + !Array.isArray(settings[game][setting]) && + setting !== 'start_inventory' + ) { + errorMessage = `${game} // ${setting} has no values above zero!`; + } + + // Remove weights from options with only one possibility + if ( + Object.keys(settings[game][setting]).length === 1 && + !Array.isArray(settings[game][setting]) && + setting !== 'start_inventory' + ) { + settings[game][setting] = Object.keys(settings[game][setting])[0]; + } + + // Remove empty arrays + else if ( + ['exclude_locations', 'priority_locations', 'local_items', + 'non_local_items', 'start_hints', 'start_location_hints'].includes(setting) && + settings[game][setting].length === 0 + ) { + delete settings[game][setting]; + } + + // Remove empty start inventory + else if ( + setting === 'start_inventory' && + Object.keys(settings[game]['start_inventory']).length === 0 + ) { + delete settings[game]['start_inventory']; + } + }); + }); + + if (Object.keys(settings.game).length === 0) { + errorMessage = 'You have not chosen a game to play!'; + } + + // Remove weights if there is only one game + else if (Object.keys(settings.game).length === 1) { + settings.game = Object.keys(settings.game)[0]; + } + + // If an error occurred, alert the user and do not export the file + if (errorMessage) { + userMessage.innerText = errorMessage; + userMessage.classList.add('visible'); + userMessage.scrollIntoView({ + behavior: 'smooth', + block: 'start', + }); + return; + } + + // If no error occurred, hide the user message if it is visible + userMessage.classList.remove('visible'); + return settings; + } + + updateVisibleGames() { + Object.entries(this.current.game).forEach(([game, weight]) => { + const gameDiv = document.getElementById(`${game}-div`); + const gameOption = document.getElementById(`${game}-game-option`); + if (parseInt(weight, 10) > 0) { + gameDiv.classList.remove('invisible'); + gameOption.classList.add('jump-link'); + gameOption.addEventListener('click', () => { + const gameDiv = document.getElementById(`${game}-div`); + if (gameDiv.classList.contains('invisible')) { return; } + gameDiv.scrollIntoView({ + behavior: 'smooth', + block: 'start', + }); + }); + } else { + gameDiv.classList.add('invisible'); + gameOption.classList.remove('jump-link'); + } + }); + } + + updateBaseSetting(event) { + const setting = event.target.getAttribute('data-setting'); + const option = event.target.getAttribute('data-option'); + const type = event.target.getAttribute('data-type'); + + switch(type){ + case 'weight': + this.current[setting][option] = isNaN(event.target.value) ? event.target.value : parseInt(event.target.value, 10); + document.getElementById(`${setting}-${option}`).innerText = event.target.value; + break; + case 'data': + this.current[setting] = isNaN(event.target.value) ? event.target.value : parseInt(event.target.value, 10); + break; + } + + this.save(); + } + + export() { + const settings = this.#validateSettings(); + if (!settings) { return; } + + const yamlText = jsyaml.safeDump(settings, { noCompatMode: true }).replaceAll(/'(\d+)':/g, (x, y) => `${y}:`); + download(`${document.getElementById('player-name').value}.yaml`, yamlText); + } + + generateGame(raceMode = false) { + const settings = this.#validateSettings(); + if (!settings) { return; } + + axios.post('/api/generate', { + weights: { player: JSON.stringify(settings) }, + presetData: { player: JSON.stringify(settings) }, + playerCount: 1, + spoiler: 3, + race: raceMode ? '1' : '0', + }).then((response) => { + window.location.href = response.data.url; + }).catch((error) => { + const userMessage = document.getElementById('user-message'); + userMessage.innerText = 'Something went wrong and your game could not be generated.'; + if (error.response.data.text) { + userMessage.innerText += ' ' + error.response.data.text; + } + userMessage.classList.add('visible'); + userMessage.scrollIntoView({ + behavior: 'smooth', + block: 'start', + }); + console.error(error); + }); + } +} + +// Settings for an individual game. +class GameSettings { + // The WeightedSettings that contains this game's settings. Used to save + // settings after editing. + #allSettings; + + // The name of this game. + name; + + // The data from the server describing the types of settings available for + // this game, as a JSON-safe blob. + get data() { + return this.#allSettings.data.games[this.name]; + } + + // The settings chosen by the user as they'd appear in the YAML file, stored + // to and retrieved from local storage. + get current() { + return this.#allSettings.current[this.name]; + } + + constructor(allSettings, name) { + this.#allSettings = allSettings; + this.name = name; + } + + // Builds and returns the settings UI for this game. + buildUI() { // Create game div, invisible by default const gameDiv = document.createElement('div'); - gameDiv.setAttribute('id', `${game}-div`); + gameDiv.setAttribute('id', `${this.name}-div`); gameDiv.classList.add('game-div'); gameDiv.classList.add('invisible'); const gameHeader = document.createElement('h2'); - gameHeader.innerText = game; + gameHeader.innerText = this.name; gameDiv.appendChild(gameHeader); const collapseButton = document.createElement('a'); @@ -137,24 +421,22 @@ const buildUI = (settingData) => { expandButton.classList.add('invisible'); gameDiv.appendChild(expandButton); - settingData.games[game].gameItems.sort((a, b) => (a > b ? 1 : (a < b ? -1 : 0))); - settingData.games[game].gameLocations.sort((a, b) => (a > b ? 1 : (a < b ? -1 : 0))); + // Sort items and locations alphabetically. + this.data.gameItems.sort(); + this.data.gameLocations.sort(); - const weightedSettingsDiv = buildWeightedSettingsDiv(game, settingData.games[game].gameSettings, - settingData.games[game].gameItems, settingData.games[game].gameLocations); + const weightedSettingsDiv = this.#buildWeightedSettingsDiv(); gameDiv.appendChild(weightedSettingsDiv); - const itemPoolDiv = buildItemsDiv(game, settingData.games[game].gameItems); + const itemPoolDiv = this.#buildItemsDiv(); gameDiv.appendChild(itemPoolDiv); - const hintsDiv = buildHintsDiv(game, settingData.games[game].gameItems, settingData.games[game].gameLocations); + const hintsDiv = this.#buildHintsDiv(); gameDiv.appendChild(hintsDiv); - const locationsDiv = buildLocationsDiv(game, settingData.games[game].gameLocations); + const locationsDiv = this.#buildLocationsDiv(); gameDiv.appendChild(locationsDiv); - gamesWrapper.appendChild(gameDiv); - collapseButton.addEventListener('click', () => { collapseButton.classList.add('invisible'); weightedSettingsDiv.classList.add('invisible'); @@ -172,257 +454,145 @@ const buildUI = (settingData) => { locationsDiv.classList.remove('invisible'); expandButton.classList.add('invisible'); }); - }); -}; -const buildGameChoice = (games) => { - const settings = JSON.parse(localStorage.getItem('weighted-settings')); - const gameChoiceDiv = document.getElementById('game-choice'); - const h2 = document.createElement('h2'); - h2.innerText = 'Game Select'; - gameChoiceDiv.appendChild(h2); + return gameDiv; + } - const gameSelectDescription = document.createElement('p'); - gameSelectDescription.classList.add('setting-description'); - gameSelectDescription.innerText = 'Choose which games you might be required to play.'; - gameChoiceDiv.appendChild(gameSelectDescription); + #buildWeightedSettingsDiv() { + const settingsWrapper = document.createElement('div'); + settingsWrapper.classList.add('settings-wrapper'); - const hintText = document.createElement('p'); - hintText.classList.add('hint-text'); - hintText.innerText = 'If a game\'s value is greater than zero, you can click it\'s name to jump ' + - 'to that section.' - gameChoiceDiv.appendChild(hintText); + Object.keys(this.data.gameSettings).forEach((settingName) => { + const setting = this.data.gameSettings[settingName]; + const settingWrapper = document.createElement('div'); + settingWrapper.classList.add('setting-wrapper'); - // Build the game choice table - const table = document.createElement('table'); - const tbody = document.createElement('tbody'); + const settingNameHeader = document.createElement('h4'); + settingNameHeader.innerText = setting.displayName; + settingWrapper.appendChild(settingNameHeader); - Object.keys(games).forEach((game) => { - const tr = document.createElement('tr'); - const tdLeft = document.createElement('td'); - tdLeft.classList.add('td-left'); - const span = document.createElement('span'); - span.innerText = game; - span.setAttribute('id', `${game}-game-option`) - tdLeft.appendChild(span); - tr.appendChild(tdLeft); + const settingDescription = document.createElement('p'); + settingDescription.classList.add('setting-description'); + settingDescription.innerText = setting.description.replace(/(\n)/g, ' '); + settingWrapper.appendChild(settingDescription); - const tdMiddle = document.createElement('td'); - tdMiddle.classList.add('td-middle'); - const range = document.createElement('input'); - range.setAttribute('type', 'range'); - range.setAttribute('min', 0); - range.setAttribute('max', 50); - range.setAttribute('data-type', 'weight'); - range.setAttribute('data-setting', 'game'); - range.setAttribute('data-option', game); - range.value = settings.game[game]; - range.addEventListener('change', (evt) => { - updateBaseSetting(evt); - updateVisibleGames(); // Show or hide games based on the new settings - }); - tdMiddle.appendChild(range); - tr.appendChild(tdMiddle); + switch(setting.type){ + case 'select': + const optionTable = document.createElement('table'); + const tbody = document.createElement('tbody'); - const tdRight = document.createElement('td'); - tdRight.setAttribute('id', `game-${game}`) - tdRight.classList.add('td-right'); - tdRight.innerText = range.value; - tr.appendChild(tdRight); - tbody.appendChild(tr); - }); - - table.appendChild(tbody); - gameChoiceDiv.appendChild(table); -}; - -const buildWeightedSettingsDiv = (game, settings, gameItems, gameLocations) => { - const currentSettings = JSON.parse(localStorage.getItem('weighted-settings')); - const settingsWrapper = document.createElement('div'); - settingsWrapper.classList.add('settings-wrapper'); - - Object.keys(settings).forEach((settingName) => { - const setting = settings[settingName]; - const settingWrapper = document.createElement('div'); - settingWrapper.classList.add('setting-wrapper'); - - const settingNameHeader = document.createElement('h4'); - settingNameHeader.innerText = setting.displayName; - settingWrapper.appendChild(settingNameHeader); - - const settingDescription = document.createElement('p'); - settingDescription.classList.add('setting-description'); - settingDescription.innerText = setting.description.replace(/(\n)/g, ' '); - settingWrapper.appendChild(settingDescription); - - switch(setting.type){ - case 'select': - const optionTable = document.createElement('table'); - const tbody = document.createElement('tbody'); - - // Add a weight range for each option - setting.options.forEach((option) => { - const tr = document.createElement('tr'); - const tdLeft = document.createElement('td'); - tdLeft.classList.add('td-left'); - tdLeft.innerText = option.name; - tr.appendChild(tdLeft); - - const tdMiddle = document.createElement('td'); - tdMiddle.classList.add('td-middle'); - const range = document.createElement('input'); - range.setAttribute('type', 'range'); - range.setAttribute('data-game', game); - range.setAttribute('data-setting', settingName); - range.setAttribute('data-option', option.value); - range.setAttribute('data-type', setting.type); - range.setAttribute('min', 0); - range.setAttribute('max', 50); - range.addEventListener('change', updateRangeSetting); - range.value = currentSettings[game][settingName][option.value]; - tdMiddle.appendChild(range); - tr.appendChild(tdMiddle); - - const tdRight = document.createElement('td'); - tdRight.setAttribute('id', `${game}-${settingName}-${option.value}`) - tdRight.classList.add('td-right'); - tdRight.innerText = range.value; - tr.appendChild(tdRight); - - tbody.appendChild(tr); - }); - - optionTable.appendChild(tbody); - settingWrapper.appendChild(optionTable); - break; - - case 'range': - case 'special_range': - const rangeTable = document.createElement('table'); - const rangeTbody = document.createElement('tbody'); - - if (((setting.max - setting.min) + 1) < 11) { - for (let i=setting.min; i <= setting.max; ++i) { + // Add a weight range for each option + setting.options.forEach((option) => { const tr = document.createElement('tr'); const tdLeft = document.createElement('td'); tdLeft.classList.add('td-left'); - tdLeft.innerText = i; + tdLeft.innerText = option.name; tr.appendChild(tdLeft); const tdMiddle = document.createElement('td'); tdMiddle.classList.add('td-middle'); const range = document.createElement('input'); range.setAttribute('type', 'range'); - range.setAttribute('id', `${game}-${settingName}-${i}-range`); - range.setAttribute('data-game', game); + range.setAttribute('data-game', this.name); range.setAttribute('data-setting', settingName); - range.setAttribute('data-option', i); + range.setAttribute('data-option', option.value); + range.setAttribute('data-type', setting.type); range.setAttribute('min', 0); range.setAttribute('max', 50); - range.addEventListener('change', updateRangeSetting); - range.value = currentSettings[game][settingName][i] || 0; + range.addEventListener('change', (evt) => this.#updateRangeSetting(evt)); + range.value = this.current[settingName][option.value]; tdMiddle.appendChild(range); tr.appendChild(tdMiddle); const tdRight = document.createElement('td'); - tdRight.setAttribute('id', `${game}-${settingName}-${i}`) + tdRight.setAttribute('id', `${this.name}-${settingName}-${option.value}`); tdRight.classList.add('td-right'); tdRight.innerText = range.value; tr.appendChild(tdRight); - rangeTbody.appendChild(tr); - } - } else { - const hintText = document.createElement('p'); - hintText.classList.add('hint-text'); - hintText.innerHTML = 'This is a range option. You may enter a valid numerical value in the text box ' + - `below, then press the "Add" button to add a weight for it.
Minimum value: ${setting.min}
` + - `Maximum value: ${setting.max}`; - - if (setting.hasOwnProperty('value_names')) { - hintText.innerHTML += '

Certain values have special meaning:'; - Object.keys(setting.value_names).forEach((specialName) => { - hintText.innerHTML += `
${specialName}: ${setting.value_names[specialName]}`; - }); - } - - settingWrapper.appendChild(hintText); - - const addOptionDiv = document.createElement('div'); - addOptionDiv.classList.add('add-option-div'); - const optionInput = document.createElement('input'); - optionInput.setAttribute('id', `${game}-${settingName}-option`); - optionInput.setAttribute('placeholder', `${setting.min} - ${setting.max}`); - addOptionDiv.appendChild(optionInput); - const addOptionButton = document.createElement('button'); - addOptionButton.innerText = 'Add'; - addOptionDiv.appendChild(addOptionButton); - settingWrapper.appendChild(addOptionDiv); - optionInput.addEventListener('keydown', (evt) => { - if (evt.key === 'Enter') { addOptionButton.dispatchEvent(new Event('click')); } + tbody.appendChild(tr); }); - addOptionButton.addEventListener('click', () => { - const optionInput = document.getElementById(`${game}-${settingName}-option`); - let option = optionInput.value; - if (!option || !option.trim()) { return; } - option = parseInt(option, 10); - if ((option < setting.min) || (option > setting.max)) { return; } - optionInput.value = ''; - if (document.getElementById(`${game}-${settingName}-${option}-range`)) { return; } + optionTable.appendChild(tbody); + settingWrapper.appendChild(optionTable); + break; - const tr = document.createElement('tr'); - const tdLeft = document.createElement('td'); - tdLeft.classList.add('td-left'); - tdLeft.innerText = option; - tr.appendChild(tdLeft); + case 'range': + case 'special_range': + const rangeTable = document.createElement('table'); + const rangeTbody = document.createElement('tbody'); - const tdMiddle = document.createElement('td'); - tdMiddle.classList.add('td-middle'); - const range = document.createElement('input'); - range.setAttribute('type', 'range'); - range.setAttribute('id', `${game}-${settingName}-${option}-range`); - range.setAttribute('data-game', game); - range.setAttribute('data-setting', settingName); - range.setAttribute('data-option', option); - range.setAttribute('min', 0); - range.setAttribute('max', 50); - range.addEventListener('change', updateRangeSetting); - range.value = currentSettings[game][settingName][parseInt(option, 10)]; - tdMiddle.appendChild(range); - tr.appendChild(tdMiddle); + if (((setting.max - setting.min) + 1) < 11) { + for (let i=setting.min; i <= setting.max; ++i) { + const tr = document.createElement('tr'); + const tdLeft = document.createElement('td'); + tdLeft.classList.add('td-left'); + tdLeft.innerText = i; + tr.appendChild(tdLeft); - const tdRight = document.createElement('td'); - tdRight.setAttribute('id', `${game}-${settingName}-${option}`) - tdRight.classList.add('td-right'); - tdRight.innerText = range.value; - tr.appendChild(tdRight); + const tdMiddle = document.createElement('td'); + tdMiddle.classList.add('td-middle'); + const range = document.createElement('input'); + range.setAttribute('type', 'range'); + range.setAttribute('id', `${this.name}-${settingName}-${i}-range`); + range.setAttribute('data-game', this.name); + range.setAttribute('data-setting', settingName); + range.setAttribute('data-option', i); + range.setAttribute('min', 0); + range.setAttribute('max', 50); + range.addEventListener('change', (evt) => this.#updateRangeSetting(evt)); + range.value = this.current[settingName][i] || 0; + tdMiddle.appendChild(range); + tr.appendChild(tdMiddle); - const tdDelete = document.createElement('td'); - tdDelete.classList.add('td-delete'); - const deleteButton = document.createElement('span'); - deleteButton.classList.add('range-option-delete'); - deleteButton.innerText = '❌'; - deleteButton.addEventListener('click', () => { - range.value = 0; - range.dispatchEvent(new Event('change')); - rangeTbody.removeChild(tr); + const tdRight = document.createElement('td'); + tdRight.setAttribute('id', `${this.name}-${settingName}-${i}`) + tdRight.classList.add('td-right'); + tdRight.innerText = range.value; + tr.appendChild(tdRight); + + rangeTbody.appendChild(tr); + } + } else { + const hintText = document.createElement('p'); + hintText.classList.add('hint-text'); + hintText.innerHTML = 'This is a range option. You may enter a valid numerical value in the text box ' + + `below, then press the "Add" button to add a weight for it.
Minimum value: ${setting.min}
` + + `Maximum value: ${setting.max}`; + + if (setting.hasOwnProperty('value_names')) { + hintText.innerHTML += '

Certain values have special meaning:'; + Object.keys(setting.value_names).forEach((specialName) => { + hintText.innerHTML += `
${specialName}: ${setting.value_names[specialName]}`; + }); + } + + settingWrapper.appendChild(hintText); + + const addOptionDiv = document.createElement('div'); + addOptionDiv.classList.add('add-option-div'); + const optionInput = document.createElement('input'); + optionInput.setAttribute('id', `${this.name}-${settingName}-option`); + optionInput.setAttribute('placeholder', `${setting.min} - ${setting.max}`); + addOptionDiv.appendChild(optionInput); + const addOptionButton = document.createElement('button'); + addOptionButton.innerText = 'Add'; + addOptionDiv.appendChild(addOptionButton); + settingWrapper.appendChild(addOptionDiv); + optionInput.addEventListener('keydown', (evt) => { + if (evt.key === 'Enter') { addOptionButton.dispatchEvent(new Event('click')); } }); - tdDelete.appendChild(deleteButton); - tr.appendChild(tdDelete); - rangeTbody.appendChild(tr); + addOptionButton.addEventListener('click', () => { + const optionInput = document.getElementById(`${this.name}-${settingName}-option`); + let option = optionInput.value; + if (!option || !option.trim()) { return; } + option = parseInt(option, 10); + if ((option < setting.min) || (option > setting.max)) { return; } + optionInput.value = ''; + if (document.getElementById(`${this.name}-${settingName}-${option}-range`)) { return; } - // Save new option to settings - range.dispatchEvent(new Event('change')); - }); - - Object.keys(currentSettings[game][settingName]).forEach((option) => { - // These options are statically generated below, and should always appear even if they are deleted - // from localStorage - if (['random-low', 'random', 'random-high'].includes(option)) { return; } - - const tr = document.createElement('tr'); + const tr = document.createElement('tr'); const tdLeft = document.createElement('td'); tdLeft.classList.add('td-left'); tdLeft.innerText = option; @@ -432,19 +602,19 @@ const buildWeightedSettingsDiv = (game, settings, gameItems, gameLocations) => { tdMiddle.classList.add('td-middle'); const range = document.createElement('input'); range.setAttribute('type', 'range'); - range.setAttribute('id', `${game}-${settingName}-${option}-range`); - range.setAttribute('data-game', game); + range.setAttribute('id', `${this.name}-${settingName}-${option}-range`); + range.setAttribute('data-game', this.name); range.setAttribute('data-setting', settingName); range.setAttribute('data-option', option); range.setAttribute('min', 0); range.setAttribute('max', 50); - range.addEventListener('change', updateRangeSetting); - range.value = currentSettings[game][settingName][parseInt(option, 10)]; + range.addEventListener('change', (evt) => this.#updateRangeSetting(evt)); + range.value = this.current[settingName][parseInt(option, 10)]; tdMiddle.appendChild(range); tr.appendChild(tdMiddle); const tdRight = document.createElement('td'); - tdRight.setAttribute('id', `${game}-${settingName}-${option}`) + tdRight.setAttribute('id', `${this.name}-${settingName}-${option}`) tdRight.classList.add('td-right'); tdRight.innerText = range.value; tr.appendChild(tdRight); @@ -456,762 +626,651 @@ const buildWeightedSettingsDiv = (game, settings, gameItems, gameLocations) => { deleteButton.innerText = '❌'; deleteButton.addEventListener('click', () => { range.value = 0; - const changeEvent = new Event('change'); - changeEvent.action = 'rangeDelete'; - range.dispatchEvent(changeEvent); + range.dispatchEvent(new Event('change')); rangeTbody.removeChild(tr); }); tdDelete.appendChild(deleteButton); tr.appendChild(tdDelete); rangeTbody.appendChild(tr); + + // Save new option to settings + range.dispatchEvent(new Event('change')); + }); + + Object.keys(this.current[settingName]).forEach((option) => { + // These options are statically generated below, and should always appear even if they are deleted + // from localStorage + if (['random-low', 'random', 'random-high'].includes(option)) { return; } + + const tr = document.createElement('tr'); + const tdLeft = document.createElement('td'); + tdLeft.classList.add('td-left'); + tdLeft.innerText = option; + tr.appendChild(tdLeft); + + const tdMiddle = document.createElement('td'); + tdMiddle.classList.add('td-middle'); + const range = document.createElement('input'); + range.setAttribute('type', 'range'); + range.setAttribute('id', `${this.name}-${settingName}-${option}-range`); + range.setAttribute('data-game', this.name); + range.setAttribute('data-setting', settingName); + range.setAttribute('data-option', option); + range.setAttribute('min', 0); + range.setAttribute('max', 50); + range.addEventListener('change', (evt) => this.#updateRangeSetting(evt)); + range.value = this.current[settingName][parseInt(option, 10)]; + tdMiddle.appendChild(range); + tr.appendChild(tdMiddle); + + const tdRight = document.createElement('td'); + tdRight.setAttribute('id', `${this.name}-${settingName}-${option}`) + tdRight.classList.add('td-right'); + tdRight.innerText = range.value; + tr.appendChild(tdRight); + + const tdDelete = document.createElement('td'); + tdDelete.classList.add('td-delete'); + const deleteButton = document.createElement('span'); + deleteButton.classList.add('range-option-delete'); + deleteButton.innerText = '❌'; + deleteButton.addEventListener('click', () => { + range.value = 0; + const changeEvent = new Event('change'); + changeEvent.action = 'rangeDelete'; + range.dispatchEvent(changeEvent); + rangeTbody.removeChild(tr); + }); + tdDelete.appendChild(deleteButton); + tr.appendChild(tdDelete); + + rangeTbody.appendChild(tr); + }); + } + + ['random', 'random-low', 'random-high'].forEach((option) => { + const tr = document.createElement('tr'); + const tdLeft = document.createElement('td'); + tdLeft.classList.add('td-left'); + switch(option){ + case 'random': + tdLeft.innerText = 'Random'; + break; + case 'random-low': + tdLeft.innerText = "Random (Low)"; + break; + case 'random-high': + tdLeft.innerText = "Random (High)"; + break; + } + tr.appendChild(tdLeft); + + const tdMiddle = document.createElement('td'); + tdMiddle.classList.add('td-middle'); + const range = document.createElement('input'); + range.setAttribute('type', 'range'); + range.setAttribute('id', `${this.name}-${settingName}-${option}-range`); + range.setAttribute('data-game', this.name); + range.setAttribute('data-setting', settingName); + range.setAttribute('data-option', option); + range.setAttribute('min', 0); + range.setAttribute('max', 50); + range.addEventListener('change', (evt) => this.#updateRangeSetting(evt)); + range.value = this.current[settingName][option]; + tdMiddle.appendChild(range); + tr.appendChild(tdMiddle); + + const tdRight = document.createElement('td'); + tdRight.setAttribute('id', `${this.name}-${settingName}-${option}`) + tdRight.classList.add('td-right'); + tdRight.innerText = range.value; + tr.appendChild(tdRight); + rangeTbody.appendChild(tr); }); - } - ['random', 'random-low', 'random-high'].forEach((option) => { - const tr = document.createElement('tr'); - const tdLeft = document.createElement('td'); - tdLeft.classList.add('td-left'); - switch(option){ - case 'random': - tdLeft.innerText = 'Random'; - break; - case 'random-low': - tdLeft.innerText = "Random (Low)"; - break; - case 'random-high': - tdLeft.innerText = "Random (High)"; - break; + rangeTable.appendChild(rangeTbody); + settingWrapper.appendChild(rangeTable); + break; + + case 'items-list': + const itemsList = document.createElement('div'); + itemsList.classList.add('simple-list'); + + Object.values(this.data.gameItems).forEach((item) => { + const itemRow = document.createElement('div'); + itemRow.classList.add('list-row'); + + const itemLabel = document.createElement('label'); + itemLabel.setAttribute('for', `${this.name}-${settingName}-${item}`) + + const itemCheckbox = document.createElement('input'); + itemCheckbox.setAttribute('id', `${this.name}-${settingName}-${item}`); + itemCheckbox.setAttribute('type', 'checkbox'); + itemCheckbox.setAttribute('data-game', this.name); + itemCheckbox.setAttribute('data-setting', settingName); + itemCheckbox.setAttribute('data-option', item.toString()); + itemCheckbox.addEventListener('change', (evt) => this.#updateListSetting(evt)); + if (this.current[settingName].includes(item)) { + itemCheckbox.setAttribute('checked', '1'); } - tr.appendChild(tdLeft); - const tdMiddle = document.createElement('td'); - tdMiddle.classList.add('td-middle'); - const range = document.createElement('input'); - range.setAttribute('type', 'range'); - range.setAttribute('id', `${game}-${settingName}-${option}-range`); - range.setAttribute('data-game', game); - range.setAttribute('data-setting', settingName); - range.setAttribute('data-option', option); - range.setAttribute('min', 0); - range.setAttribute('max', 50); - range.addEventListener('change', updateRangeSetting); - range.value = currentSettings[game][settingName][option]; - tdMiddle.appendChild(range); - tr.appendChild(tdMiddle); + const itemName = document.createElement('span'); + itemName.innerText = item.toString(); - const tdRight = document.createElement('td'); - tdRight.setAttribute('id', `${game}-${settingName}-${option}`) - tdRight.classList.add('td-right'); - tdRight.innerText = range.value; - tr.appendChild(tdRight); - rangeTbody.appendChild(tr); - }); + itemLabel.appendChild(itemCheckbox); + itemLabel.appendChild(itemName); - rangeTable.appendChild(rangeTbody); - settingWrapper.appendChild(rangeTable); - break; + itemRow.appendChild(itemLabel); + itemsList.appendChild((itemRow)); + }); - case 'items-list': - const itemsList = document.createElement('div'); - itemsList.classList.add('simple-list'); + settingWrapper.appendChild(itemsList); + break; - Object.values(gameItems).forEach((item) => { - const itemRow = document.createElement('div'); - itemRow.classList.add('list-row'); + case 'locations-list': + const locationsList = document.createElement('div'); + locationsList.classList.add('simple-list'); - const itemLabel = document.createElement('label'); - itemLabel.setAttribute('for', `${game}-${settingName}-${item}`) + Object.values(this.data.gameLocations).forEach((location) => { + const locationRow = document.createElement('div'); + locationRow.classList.add('list-row'); - const itemCheckbox = document.createElement('input'); - itemCheckbox.setAttribute('id', `${game}-${settingName}-${item}`); - itemCheckbox.setAttribute('type', 'checkbox'); - itemCheckbox.setAttribute('data-game', game); - itemCheckbox.setAttribute('data-setting', settingName); - itemCheckbox.setAttribute('data-option', item.toString()); - itemCheckbox.addEventListener('change', updateListSetting); - if (currentSettings[game][settingName].includes(item)) { - itemCheckbox.setAttribute('checked', '1'); - } + const locationLabel = document.createElement('label'); + locationLabel.setAttribute('for', `${this.name}-${settingName}-${location}`) - const itemName = document.createElement('span'); - itemName.innerText = item.toString(); + const locationCheckbox = document.createElement('input'); + locationCheckbox.setAttribute('id', `${this.name}-${settingName}-${location}`); + locationCheckbox.setAttribute('type', 'checkbox'); + locationCheckbox.setAttribute('data-game', this.name); + locationCheckbox.setAttribute('data-setting', settingName); + locationCheckbox.setAttribute('data-option', location.toString()); + locationCheckbox.addEventListener('change', (evt) => this.#updateListSetting(evt)); + if (this.current[settingName].includes(location)) { + locationCheckbox.setAttribute('checked', '1'); + } - itemLabel.appendChild(itemCheckbox); - itemLabel.appendChild(itemName); + const locationName = document.createElement('span'); + locationName.innerText = location.toString(); - itemRow.appendChild(itemLabel); - itemsList.appendChild((itemRow)); - }); + locationLabel.appendChild(locationCheckbox); + locationLabel.appendChild(locationName); - settingWrapper.appendChild(itemsList); - break; + locationRow.appendChild(locationLabel); + locationsList.appendChild((locationRow)); + }); - case 'locations-list': - const locationsList = document.createElement('div'); - locationsList.classList.add('simple-list'); + settingWrapper.appendChild(locationsList); + break; - Object.values(gameLocations).forEach((location) => { - const locationRow = document.createElement('div'); - locationRow.classList.add('list-row'); + case 'custom-list': + const customList = document.createElement('div'); + customList.classList.add('simple-list'); - const locationLabel = document.createElement('label'); - locationLabel.setAttribute('for', `${game}-${settingName}-${location}`) + Object.values(this.data.gameSettings[settingName].options).forEach((listItem) => { + const customListRow = document.createElement('div'); + customListRow.classList.add('list-row'); - const locationCheckbox = document.createElement('input'); - locationCheckbox.setAttribute('id', `${game}-${settingName}-${location}`); - locationCheckbox.setAttribute('type', 'checkbox'); - locationCheckbox.setAttribute('data-game', game); - locationCheckbox.setAttribute('data-setting', settingName); - locationCheckbox.setAttribute('data-option', location.toString()); - locationCheckbox.addEventListener('change', updateListSetting); - if (currentSettings[game][settingName].includes(location)) { - locationCheckbox.setAttribute('checked', '1'); - } + const customItemLabel = document.createElement('label'); + customItemLabel.setAttribute('for', `${this.name}-${settingName}-${listItem}`) - const locationName = document.createElement('span'); - locationName.innerText = location.toString(); + const customItemCheckbox = document.createElement('input'); + customItemCheckbox.setAttribute('id', `${this.name}-${settingName}-${listItem}`); + customItemCheckbox.setAttribute('type', 'checkbox'); + customItemCheckbox.setAttribute('data-game', this.name); + customItemCheckbox.setAttribute('data-setting', settingName); + customItemCheckbox.setAttribute('data-option', listItem.toString()); + customItemCheckbox.addEventListener('change', (evt) => this.#updateListSetting(evt)); + if (this.current[settingName].includes(listItem)) { + customItemCheckbox.setAttribute('checked', '1'); + } - locationLabel.appendChild(locationCheckbox); - locationLabel.appendChild(locationName); + const customItemName = document.createElement('span'); + customItemName.innerText = listItem.toString(); - locationRow.appendChild(locationLabel); - locationsList.appendChild((locationRow)); - }); + customItemLabel.appendChild(customItemCheckbox); + customItemLabel.appendChild(customItemName); - settingWrapper.appendChild(locationsList); - break; + customListRow.appendChild(customItemLabel); + customList.appendChild((customListRow)); + }); - case 'custom-list': - const customList = document.createElement('div'); - customList.classList.add('simple-list'); + settingWrapper.appendChild(customList); + break; - Object.values(settings[settingName].options).forEach((listItem) => { - const customListRow = document.createElement('div'); - customListRow.classList.add('list-row'); - - const customItemLabel = document.createElement('label'); - customItemLabel.setAttribute('for', `${game}-${settingName}-${listItem}`) - - const customItemCheckbox = document.createElement('input'); - customItemCheckbox.setAttribute('id', `${game}-${settingName}-${listItem}`); - customItemCheckbox.setAttribute('type', 'checkbox'); - customItemCheckbox.setAttribute('data-game', game); - customItemCheckbox.setAttribute('data-setting', settingName); - customItemCheckbox.setAttribute('data-option', listItem.toString()); - customItemCheckbox.addEventListener('change', updateListSetting); - if (currentSettings[game][settingName].includes(listItem)) { - customItemCheckbox.setAttribute('checked', '1'); - } - - const customItemName = document.createElement('span'); - customItemName.innerText = listItem.toString(); - - customItemLabel.appendChild(customItemCheckbox); - customItemLabel.appendChild(customItemName); - - customListRow.appendChild(customItemLabel); - customList.appendChild((customListRow)); - }); - - settingWrapper.appendChild(customList); - break; - - default: - console.error(`Unknown setting type for ${game} setting ${settingName}: ${setting.type}`); - return; - } - - settingsWrapper.appendChild(settingWrapper); - }); - - return settingsWrapper; -}; - -const buildItemsDiv = (game, items) => { - // Sort alphabetical, in pace - items.sort(); - - const currentSettings = JSON.parse(localStorage.getItem('weighted-settings')); - const itemsDiv = document.createElement('div'); - itemsDiv.classList.add('items-div'); - - const itemsDivHeader = document.createElement('h3'); - itemsDivHeader.innerText = 'Item Pool'; - itemsDiv.appendChild(itemsDivHeader); - - const itemsDescription = document.createElement('p'); - itemsDescription.classList.add('setting-description'); - itemsDescription.innerText = 'Choose if you would like to start with items, or control if they are placed in ' + - 'your seed or someone else\'s.'; - itemsDiv.appendChild(itemsDescription); - - const itemsHint = document.createElement('p'); - itemsHint.classList.add('hint-text'); - itemsHint.innerText = 'Drag and drop items from one box to another.'; - itemsDiv.appendChild(itemsHint); - - const itemsWrapper = document.createElement('div'); - itemsWrapper.classList.add('items-wrapper'); - - // Create container divs for each category - const availableItemsWrapper = document.createElement('div'); - availableItemsWrapper.classList.add('item-set-wrapper'); - availableItemsWrapper.innerText = 'Available Items'; - const availableItems = document.createElement('div'); - availableItems.classList.add('item-container'); - availableItems.setAttribute('id', `${game}-available_items`); - availableItems.addEventListener('dragover', itemDragoverHandler); - availableItems.addEventListener('drop', itemDropHandler); - - const startInventoryWrapper = document.createElement('div'); - startInventoryWrapper.classList.add('item-set-wrapper'); - startInventoryWrapper.innerText = 'Start Inventory'; - const startInventory = document.createElement('div'); - startInventory.classList.add('item-container'); - startInventory.setAttribute('id', `${game}-start_inventory`); - startInventory.setAttribute('data-setting', 'start_inventory'); - startInventory.addEventListener('dragover', itemDragoverHandler); - startInventory.addEventListener('drop', itemDropHandler); - - const localItemsWrapper = document.createElement('div'); - localItemsWrapper.classList.add('item-set-wrapper'); - localItemsWrapper.innerText = 'Local Items'; - const localItems = document.createElement('div'); - localItems.classList.add('item-container'); - localItems.setAttribute('id', `${game}-local_items`); - localItems.setAttribute('data-setting', 'local_items') - localItems.addEventListener('dragover', itemDragoverHandler); - localItems.addEventListener('drop', itemDropHandler); - - const nonLocalItemsWrapper = document.createElement('div'); - nonLocalItemsWrapper.classList.add('item-set-wrapper'); - nonLocalItemsWrapper.innerText = 'Non-Local Items'; - const nonLocalItems = document.createElement('div'); - nonLocalItems.classList.add('item-container'); - nonLocalItems.setAttribute('id', `${game}-non_local_items`); - nonLocalItems.setAttribute('data-setting', 'non_local_items'); - nonLocalItems.addEventListener('dragover', itemDragoverHandler); - nonLocalItems.addEventListener('drop', itemDropHandler); - - // Populate the divs - items.forEach((item) => { - if (Object.keys(currentSettings[game].start_inventory).includes(item)){ - const itemDiv = buildItemQtyDiv(game, item); - itemDiv.setAttribute('data-setting', 'start_inventory'); - startInventory.appendChild(itemDiv); - } else if (currentSettings[game].local_items.includes(item)) { - const itemDiv = buildItemDiv(game, item); - itemDiv.setAttribute('data-setting', 'local_items'); - localItems.appendChild(itemDiv); - } else if (currentSettings[game].non_local_items.includes(item)) { - const itemDiv = buildItemDiv(game, item); - itemDiv.setAttribute('data-setting', 'non_local_items'); - nonLocalItems.appendChild(itemDiv); - } else { - const itemDiv = buildItemDiv(game, item); - availableItems.appendChild(itemDiv); - } - }); - - availableItemsWrapper.appendChild(availableItems); - startInventoryWrapper.appendChild(startInventory); - localItemsWrapper.appendChild(localItems); - nonLocalItemsWrapper.appendChild(nonLocalItems); - itemsWrapper.appendChild(availableItemsWrapper); - itemsWrapper.appendChild(startInventoryWrapper); - itemsWrapper.appendChild(localItemsWrapper); - itemsWrapper.appendChild(nonLocalItemsWrapper); - itemsDiv.appendChild(itemsWrapper); - return itemsDiv; -}; - -const buildItemDiv = (game, item) => { - const itemDiv = document.createElement('div'); - itemDiv.classList.add('item-div'); - itemDiv.setAttribute('id', `${game}-${item}`); - itemDiv.setAttribute('data-game', game); - itemDiv.setAttribute('data-item', item); - itemDiv.setAttribute('draggable', 'true'); - itemDiv.innerText = item; - itemDiv.addEventListener('dragstart', (evt) => { - evt.dataTransfer.setData('text/plain', itemDiv.getAttribute('id')); - }); - return itemDiv; -}; - -const buildItemQtyDiv = (game, item) => { - const currentSettings = JSON.parse(localStorage.getItem('weighted-settings')); - const itemQtyDiv = document.createElement('div'); - itemQtyDiv.classList.add('item-qty-div'); - itemQtyDiv.setAttribute('id', `${game}-${item}`); - itemQtyDiv.setAttribute('data-game', game); - itemQtyDiv.setAttribute('data-item', item); - itemQtyDiv.setAttribute('draggable', 'true'); - itemQtyDiv.innerText = item; - - const inputWrapper = document.createElement('div'); - inputWrapper.classList.add('item-qty-input-wrapper') - - const itemQty = document.createElement('input'); - itemQty.setAttribute('value', currentSettings[game].start_inventory.hasOwnProperty(item) ? - currentSettings[game].start_inventory[item] : '1'); - itemQty.setAttribute('data-game', game); - itemQty.setAttribute('data-setting', 'start_inventory'); - itemQty.setAttribute('data-option', item); - itemQty.setAttribute('maxlength', '3'); - itemQty.addEventListener('keyup', (evt) => { - evt.target.value = isNaN(parseInt(evt.target.value)) ? 0 : parseInt(evt.target.value); - updateItemSetting(evt); - }); - inputWrapper.appendChild(itemQty); - itemQtyDiv.appendChild(inputWrapper); - - itemQtyDiv.addEventListener('dragstart', (evt) => { - evt.dataTransfer.setData('text/plain', itemQtyDiv.getAttribute('id')); - }); - return itemQtyDiv; -}; - -const itemDragoverHandler = (evt) => { - evt.preventDefault(); -}; - -const itemDropHandler = (evt) => { - evt.preventDefault(); - const sourceId = evt.dataTransfer.getData('text/plain'); - const sourceDiv = document.getElementById(sourceId); - - const currentSettings = JSON.parse(localStorage.getItem('weighted-settings')); - const game = sourceDiv.getAttribute('data-game'); - const item = sourceDiv.getAttribute('data-item'); - - const oldSetting = sourceDiv.hasAttribute('data-setting') ? sourceDiv.getAttribute('data-setting') : null; - const newSetting = evt.target.hasAttribute('data-setting') ? evt.target.getAttribute('data-setting') : null; - - const itemDiv = newSetting === 'start_inventory' ? buildItemQtyDiv(game, item) : buildItemDiv(game, item); - - if (oldSetting) { - if (oldSetting === 'start_inventory') { - if (currentSettings[game][oldSetting].hasOwnProperty(item)) { - delete currentSettings[game][oldSetting][item]; + default: + console.error(`Unknown setting type for ${this.name} setting ${settingName}: ${setting.type}`); + return; } - } else { - if (currentSettings[game][oldSetting].includes(item)) { - currentSettings[game][oldSetting].splice(currentSettings[game][oldSetting].indexOf(item), 1); - } - } - } - if (newSetting) { - itemDiv.setAttribute('data-setting', newSetting); - document.getElementById(`${game}-${newSetting}`).appendChild(itemDiv); - if (newSetting === 'start_inventory') { - currentSettings[game][newSetting][item] = 1; - } else { - if (!currentSettings[game][newSetting].includes(item)){ - currentSettings[game][newSetting].push(item); - } - } - } else { - // No setting was assigned, this item has been removed from the settings - document.getElementById(`${game}-available_items`).appendChild(itemDiv); - } - - // Remove the source drag object - sourceDiv.parentElement.removeChild(sourceDiv); - - // Save the updated settings - localStorage.setItem('weighted-settings', JSON.stringify(currentSettings)); -}; - -const buildHintsDiv = (game, items, locations) => { - const currentSettings = JSON.parse(localStorage.getItem('weighted-settings')); - - // Sort alphabetical, in place - items.sort(); - locations.sort(); - - const hintsDiv = document.createElement('div'); - hintsDiv.classList.add('hints-div'); - const hintsHeader = document.createElement('h3'); - hintsHeader.innerText = 'Item & Location Hints'; - hintsDiv.appendChild(hintsHeader); - const hintsDescription = document.createElement('p'); - hintsDescription.classList.add('setting-description'); - hintsDescription.innerText = 'Choose any items or locations to begin the game with the knowledge of where those ' + - ' items are, or what those locations contain.'; - hintsDiv.appendChild(hintsDescription); - - const itemHintsContainer = document.createElement('div'); - itemHintsContainer.classList.add('hints-container'); - - // Item Hints - const itemHintsWrapper = document.createElement('div'); - itemHintsWrapper.classList.add('hints-wrapper'); - itemHintsWrapper.innerText = 'Starting Item Hints'; - - const itemHintsDiv = document.createElement('div'); - itemHintsDiv.classList.add('simple-list'); - items.forEach((item) => { - const itemRow = document.createElement('div'); - itemRow.classList.add('list-row'); - - const itemLabel = document.createElement('label'); - itemLabel.setAttribute('for', `${game}-start_hints-${item}`); - - const itemCheckbox = document.createElement('input'); - itemCheckbox.setAttribute('type', 'checkbox'); - itemCheckbox.setAttribute('id', `${game}-start_hints-${item}`); - itemCheckbox.setAttribute('data-game', game); - itemCheckbox.setAttribute('data-setting', 'start_hints'); - itemCheckbox.setAttribute('data-option', item); - if (currentSettings[game].start_hints.includes(item)) { - itemCheckbox.setAttribute('checked', 'true'); - } - itemCheckbox.addEventListener('change', updateListSetting); - itemLabel.appendChild(itemCheckbox); - - const itemName = document.createElement('span'); - itemName.innerText = item; - itemLabel.appendChild(itemName); - - itemRow.appendChild(itemLabel); - itemHintsDiv.appendChild(itemRow); - }); - - itemHintsWrapper.appendChild(itemHintsDiv); - itemHintsContainer.appendChild(itemHintsWrapper); - - // Starting Location Hints - const locationHintsWrapper = document.createElement('div'); - locationHintsWrapper.classList.add('hints-wrapper'); - locationHintsWrapper.innerText = 'Starting Location Hints'; - - const locationHintsDiv = document.createElement('div'); - locationHintsDiv.classList.add('simple-list'); - locations.forEach((location) => { - const locationRow = document.createElement('div'); - locationRow.classList.add('list-row'); - - const locationLabel = document.createElement('label'); - locationLabel.setAttribute('for', `${game}-start_location_hints-${location}`); - - const locationCheckbox = document.createElement('input'); - locationCheckbox.setAttribute('type', 'checkbox'); - locationCheckbox.setAttribute('id', `${game}-start_location_hints-${location}`); - locationCheckbox.setAttribute('data-game', game); - locationCheckbox.setAttribute('data-setting', 'start_location_hints'); - locationCheckbox.setAttribute('data-option', location); - if (currentSettings[game].start_location_hints.includes(location)) { - locationCheckbox.setAttribute('checked', '1'); - } - locationCheckbox.addEventListener('change', updateListSetting); - locationLabel.appendChild(locationCheckbox); - - const locationName = document.createElement('span'); - locationName.innerText = location; - locationLabel.appendChild(locationName); - - locationRow.appendChild(locationLabel); - locationHintsDiv.appendChild(locationRow); - }); - - locationHintsWrapper.appendChild(locationHintsDiv); - itemHintsContainer.appendChild(locationHintsWrapper); - - hintsDiv.appendChild(itemHintsContainer); - return hintsDiv; -}; - -const buildLocationsDiv = (game, locations) => { - const currentSettings = JSON.parse(localStorage.getItem('weighted-settings')); - locations.sort(); // Sort alphabetical, in-place - - const locationsDiv = document.createElement('div'); - locationsDiv.classList.add('locations-div'); - const locationsHeader = document.createElement('h3'); - locationsHeader.innerText = 'Priority & Exclusion Locations'; - locationsDiv.appendChild(locationsHeader); - const locationsDescription = document.createElement('p'); - locationsDescription.classList.add('setting-description'); - locationsDescription.innerText = 'Priority locations guarantee a progression item will be placed there while ' + - 'excluded locations will not contain progression or useful items.'; - locationsDiv.appendChild(locationsDescription); - - const locationsContainer = document.createElement('div'); - locationsContainer.classList.add('locations-container'); - - // Priority Locations - const priorityLocationsWrapper = document.createElement('div'); - priorityLocationsWrapper.classList.add('locations-wrapper'); - priorityLocationsWrapper.innerText = 'Priority Locations'; - - const priorityLocationsDiv = document.createElement('div'); - priorityLocationsDiv.classList.add('simple-list'); - locations.forEach((location) => { - const locationRow = document.createElement('div'); - locationRow.classList.add('list-row'); - - const locationLabel = document.createElement('label'); - locationLabel.setAttribute('for', `${game}-priority_locations-${location}`); - - const locationCheckbox = document.createElement('input'); - locationCheckbox.setAttribute('type', 'checkbox'); - locationCheckbox.setAttribute('id', `${game}-priority_locations-${location}`); - locationCheckbox.setAttribute('data-game', game); - locationCheckbox.setAttribute('data-setting', 'priority_locations'); - locationCheckbox.setAttribute('data-option', location); - if (currentSettings[game].priority_locations.includes(location)) { - locationCheckbox.setAttribute('checked', '1'); - } - locationCheckbox.addEventListener('change', updateListSetting); - locationLabel.appendChild(locationCheckbox); - - const locationName = document.createElement('span'); - locationName.innerText = location; - locationLabel.appendChild(locationName); - - locationRow.appendChild(locationLabel); - priorityLocationsDiv.appendChild(locationRow); - }); - - priorityLocationsWrapper.appendChild(priorityLocationsDiv); - locationsContainer.appendChild(priorityLocationsWrapper); - - // Exclude Locations - const excludeLocationsWrapper = document.createElement('div'); - excludeLocationsWrapper.classList.add('locations-wrapper'); - excludeLocationsWrapper.innerText = 'Exclude Locations'; - - const excludeLocationsDiv = document.createElement('div'); - excludeLocationsDiv.classList.add('simple-list'); - locations.forEach((location) => { - const locationRow = document.createElement('div'); - locationRow.classList.add('list-row'); - - const locationLabel = document.createElement('label'); - locationLabel.setAttribute('for', `${game}-exclude_locations-${location}`); - - const locationCheckbox = document.createElement('input'); - locationCheckbox.setAttribute('type', 'checkbox'); - locationCheckbox.setAttribute('id', `${game}-exclude_locations-${location}`); - locationCheckbox.setAttribute('data-game', game); - locationCheckbox.setAttribute('data-setting', 'exclude_locations'); - locationCheckbox.setAttribute('data-option', location); - if (currentSettings[game].exclude_locations.includes(location)) { - locationCheckbox.setAttribute('checked', '1'); - } - locationCheckbox.addEventListener('change', updateListSetting); - locationLabel.appendChild(locationCheckbox); - - const locationName = document.createElement('span'); - locationName.innerText = location; - locationLabel.appendChild(locationName); - - locationRow.appendChild(locationLabel); - excludeLocationsDiv.appendChild(locationRow); - }); - - excludeLocationsWrapper.appendChild(excludeLocationsDiv); - locationsContainer.appendChild(excludeLocationsWrapper); - - locationsDiv.appendChild(locationsContainer); - return locationsDiv; -}; - -const updateVisibleGames = () => { - const settings = JSON.parse(localStorage.getItem('weighted-settings')); - Object.keys(settings.game).forEach((game) => { - const gameDiv = document.getElementById(`${game}-div`); - const gameOption = document.getElementById(`${game}-game-option`); - if (parseInt(settings.game[game], 10) > 0) { - gameDiv.classList.remove('invisible'); - gameOption.classList.add('jump-link'); - gameOption.addEventListener('click', () => { - const gameDiv = document.getElementById(`${game}-div`); - if (gameDiv.classList.contains('invisible')) { return; } - gameDiv.scrollIntoView({ - behavior: 'smooth', - block: 'start', - }); - }); - } else { - gameDiv.classList.add('invisible'); - gameOption.classList.remove('jump-link'); - - } - }); -}; - -const updateBaseSetting = (event) => { - const settings = JSON.parse(localStorage.getItem('weighted-settings')); - const setting = event.target.getAttribute('data-setting'); - const option = event.target.getAttribute('data-option'); - const type = event.target.getAttribute('data-type'); - - switch(type){ - case 'weight': - settings[setting][option] = isNaN(event.target.value) ? event.target.value : parseInt(event.target.value, 10); - document.getElementById(`${setting}-${option}`).innerText = event.target.value; - break; - case 'data': - settings[setting] = isNaN(event.target.value) ? event.target.value : parseInt(event.target.value, 10); - break; - } - - localStorage.setItem('weighted-settings', JSON.stringify(settings)); -}; - -const updateRangeSetting = (evt) => { - const options = JSON.parse(localStorage.getItem('weighted-settings')); - const game = evt.target.getAttribute('data-game'); - const setting = evt.target.getAttribute('data-setting'); - const option = evt.target.getAttribute('data-option'); - document.getElementById(`${game}-${setting}-${option}`).innerText = evt.target.value; - if (evt.action && evt.action === 'rangeDelete') { - delete options[game][setting][option]; - } else { - options[game][setting][option] = parseInt(evt.target.value, 10); - } - localStorage.setItem('weighted-settings', JSON.stringify(options)); -}; - -const updateListSetting = (evt) => { - const options = JSON.parse(localStorage.getItem('weighted-settings')); - const game = evt.target.getAttribute('data-game'); - const setting = evt.target.getAttribute('data-setting'); - const option = evt.target.getAttribute('data-option'); - - if (evt.target.checked) { - // If the option is to be enabled and it is already enabled, do nothing - if (options[game][setting].includes(option)) { return; } - - options[game][setting].push(option); - } else { - // If the option is to be disabled and it is already disabled, do nothing - if (!options[game][setting].includes(option)) { return; } - - options[game][setting].splice(options[game][setting].indexOf(option), 1); - } - localStorage.setItem('weighted-settings', JSON.stringify(options)); -}; - -const updateItemSetting = (evt) => { - const options = JSON.parse(localStorage.getItem('weighted-settings')); - const game = evt.target.getAttribute('data-game'); - const setting = evt.target.getAttribute('data-setting'); - const option = evt.target.getAttribute('data-option'); - if (setting === 'start_inventory') { - options[game][setting][option] = evt.target.value.trim() ? parseInt(evt.target.value) : 0; - } else { - options[game][setting][option] = isNaN(evt.target.value) ? - evt.target.value : parseInt(evt.target.value, 10); - } - localStorage.setItem('weighted-settings', JSON.stringify(options)); -}; - -const validateSettings = () => { - const settings = JSON.parse(localStorage.getItem('weighted-settings')); - const userMessage = document.getElementById('user-message'); - let errorMessage = null; - - // User must choose a name for their file - if (!settings.name || settings.name.trim().length === 0 || settings.name.toLowerCase().trim() === 'player') { - userMessage.innerText = 'You forgot to set your player name at the top of the page!'; - userMessage.classList.add('visible'); - userMessage.scrollIntoView({ - behavior: 'smooth', - block: 'start', + settingsWrapper.appendChild(settingWrapper); }); - return; + + return settingsWrapper; } - // Clean up the settings output - Object.keys(settings.game).forEach((game) => { - // Remove any disabled games - if (settings.game[game] === 0) { - delete settings.game[game]; - delete settings[game]; - return; - } + #buildItemsDiv() { + const itemsDiv = document.createElement('div'); + itemsDiv.classList.add('items-div'); - Object.keys(settings[game]).forEach((setting) => { - // Remove any disabled options - Object.keys(settings[game][setting]).forEach((option) => { - if (settings[game][setting][option] === 0) { - delete settings[game][setting][option]; + const itemsDivHeader = document.createElement('h3'); + itemsDivHeader.innerText = 'Item Pool'; + itemsDiv.appendChild(itemsDivHeader); + + const itemsDescription = document.createElement('p'); + itemsDescription.classList.add('setting-description'); + itemsDescription.innerText = 'Choose if you would like to start with items, or control if they are placed in ' + + 'your seed or someone else\'s.'; + itemsDiv.appendChild(itemsDescription); + + const itemsHint = document.createElement('p'); + itemsHint.classList.add('hint-text'); + itemsHint.innerText = 'Drag and drop items from one box to another.'; + itemsDiv.appendChild(itemsHint); + + const itemsWrapper = document.createElement('div'); + itemsWrapper.classList.add('items-wrapper'); + + const itemDragoverHandler = (evt) => evt.preventDefault(); + const itemDropHandler = (evt) => this.#itemDropHandler(evt); + + // Create container divs for each category + const availableItemsWrapper = document.createElement('div'); + availableItemsWrapper.classList.add('item-set-wrapper'); + availableItemsWrapper.innerText = 'Available Items'; + const availableItems = document.createElement('div'); + availableItems.classList.add('item-container'); + availableItems.setAttribute('id', `${this.name}-available_items`); + availableItems.addEventListener('dragover', itemDragoverHandler); + availableItems.addEventListener('drop', itemDropHandler); + + const startInventoryWrapper = document.createElement('div'); + startInventoryWrapper.classList.add('item-set-wrapper'); + startInventoryWrapper.innerText = 'Start Inventory'; + const startInventory = document.createElement('div'); + startInventory.classList.add('item-container'); + startInventory.setAttribute('id', `${this.name}-start_inventory`); + startInventory.setAttribute('data-setting', 'start_inventory'); + startInventory.addEventListener('dragover', itemDragoverHandler); + startInventory.addEventListener('drop', itemDropHandler); + + const localItemsWrapper = document.createElement('div'); + localItemsWrapper.classList.add('item-set-wrapper'); + localItemsWrapper.innerText = 'Local Items'; + const localItems = document.createElement('div'); + localItems.classList.add('item-container'); + localItems.setAttribute('id', `${this.name}-local_items`); + localItems.setAttribute('data-setting', 'local_items') + localItems.addEventListener('dragover', itemDragoverHandler); + localItems.addEventListener('drop', itemDropHandler); + + const nonLocalItemsWrapper = document.createElement('div'); + nonLocalItemsWrapper.classList.add('item-set-wrapper'); + nonLocalItemsWrapper.innerText = 'Non-Local Items'; + const nonLocalItems = document.createElement('div'); + nonLocalItems.classList.add('item-container'); + nonLocalItems.setAttribute('id', `${this.name}-non_local_items`); + nonLocalItems.setAttribute('data-setting', 'non_local_items'); + nonLocalItems.addEventListener('dragover', itemDragoverHandler); + nonLocalItems.addEventListener('drop', itemDropHandler); + + // Populate the divs + this.data.gameItems.forEach((item) => { + if (Object.keys(this.current.start_inventory).includes(item)){ + const itemDiv = this.#buildItemQtyDiv(item); + itemDiv.setAttribute('data-setting', 'start_inventory'); + startInventory.appendChild(itemDiv); + } else if (this.current.local_items.includes(item)) { + const itemDiv = this.#buildItemDiv(item); + itemDiv.setAttribute('data-setting', 'local_items'); + localItems.appendChild(itemDiv); + } else if (this.current.non_local_items.includes(item)) { + const itemDiv = this.#buildItemDiv(item); + itemDiv.setAttribute('data-setting', 'non_local_items'); + nonLocalItems.appendChild(itemDiv); + } else { + const itemDiv = this.#buildItemDiv(item); + availableItems.appendChild(itemDiv); + } + }); + + availableItemsWrapper.appendChild(availableItems); + startInventoryWrapper.appendChild(startInventory); + localItemsWrapper.appendChild(localItems); + nonLocalItemsWrapper.appendChild(nonLocalItems); + itemsWrapper.appendChild(availableItemsWrapper); + itemsWrapper.appendChild(startInventoryWrapper); + itemsWrapper.appendChild(localItemsWrapper); + itemsWrapper.appendChild(nonLocalItemsWrapper); + itemsDiv.appendChild(itemsWrapper); + return itemsDiv; + } + + #buildItemDiv(item) { + const itemDiv = document.createElement('div'); + itemDiv.classList.add('item-div'); + itemDiv.setAttribute('id', `${this.name}-${item}`); + itemDiv.setAttribute('data-game', this.name); + itemDiv.setAttribute('data-item', item); + itemDiv.setAttribute('draggable', 'true'); + itemDiv.innerText = item; + itemDiv.addEventListener('dragstart', (evt) => { + evt.dataTransfer.setData('text/plain', itemDiv.getAttribute('id')); + }); + return itemDiv; + } + + #buildItemQtyDiv(item) { + const itemQtyDiv = document.createElement('div'); + itemQtyDiv.classList.add('item-qty-div'); + itemQtyDiv.setAttribute('id', `${this.name}-${item}`); + itemQtyDiv.setAttribute('data-game', this.name); + itemQtyDiv.setAttribute('data-item', item); + itemQtyDiv.setAttribute('draggable', 'true'); + itemQtyDiv.innerText = item; + + const inputWrapper = document.createElement('div'); + inputWrapper.classList.add('item-qty-input-wrapper') + + const itemQty = document.createElement('input'); + itemQty.setAttribute('value', this.current.start_inventory.hasOwnProperty(item) ? + this.current.start_inventory[item] : '1'); + itemQty.setAttribute('data-game', this.name); + itemQty.setAttribute('data-setting', 'start_inventory'); + itemQty.setAttribute('data-option', item); + itemQty.setAttribute('maxlength', '3'); + itemQty.addEventListener('keyup', (evt) => { + evt.target.value = isNaN(parseInt(evt.target.value)) ? 0 : parseInt(evt.target.value); + this.#updateItemSetting(evt); + }); + inputWrapper.appendChild(itemQty); + itemQtyDiv.appendChild(inputWrapper); + + itemQtyDiv.addEventListener('dragstart', (evt) => { + evt.dataTransfer.setData('text/plain', itemQtyDiv.getAttribute('id')); + }); + return itemQtyDiv; + } + + #itemDropHandler(evt) { + evt.preventDefault(); + const sourceId = evt.dataTransfer.getData('text/plain'); + const sourceDiv = document.getElementById(sourceId); + + const item = sourceDiv.getAttribute('data-item'); + + const oldSetting = sourceDiv.hasAttribute('data-setting') ? sourceDiv.getAttribute('data-setting') : null; + const newSetting = evt.target.hasAttribute('data-setting') ? evt.target.getAttribute('data-setting') : null; + + const itemDiv = newSetting === 'start_inventory' ? this.#buildItemQtyDiv(item) : this.#buildItemDiv(item); + + if (oldSetting) { + if (oldSetting === 'start_inventory') { + if (this.current[oldSetting].hasOwnProperty(item)) { + delete this.current[oldSetting][item]; + } + } else { + if (this.current[oldSetting].includes(item)) { + this.current[oldSetting].splice(this.current[oldSetting].indexOf(item), 1); } - }); - - if ( - Object.keys(settings[game][setting]).length === 0 && - !Array.isArray(settings[game][setting]) && - setting !== 'start_inventory' - ) { - errorMessage = `${game} // ${setting} has no values above zero!`; } + } - // Remove weights from options with only one possibility - if ( - Object.keys(settings[game][setting]).length === 1 && - !Array.isArray(settings[game][setting]) && - setting !== 'start_inventory' - ) { - settings[game][setting] = Object.keys(settings[game][setting])[0]; + if (newSetting) { + itemDiv.setAttribute('data-setting', newSetting); + document.getElementById(`${this.name}-${newSetting}`).appendChild(itemDiv); + if (newSetting === 'start_inventory') { + this.current[newSetting][item] = 1; + } else { + if (!this.current[newSetting].includes(item)){ + this.current[newSetting].push(item); + } } + } else { + // No setting was assigned, this item has been removed from the settings + document.getElementById(`${this.name}-available_items`).appendChild(itemDiv); + } - // Remove empty arrays - else if ( - ['exclude_locations', 'priority_locations', 'local_items', - 'non_local_items', 'start_hints', 'start_location_hints'].includes(setting) && - settings[game][setting].length === 0 - ) { - delete settings[game][setting]; - } + // Remove the source drag object + sourceDiv.parentElement.removeChild(sourceDiv); - // Remove empty start inventory - else if ( - setting === 'start_inventory' && - Object.keys(settings[game]['start_inventory']).length === 0 - ) { - delete settings[game]['start_inventory']; + // Save the updated settings + this.save(); + } + + #buildHintsDiv() { + const hintsDiv = document.createElement('div'); + hintsDiv.classList.add('hints-div'); + const hintsHeader = document.createElement('h3'); + hintsHeader.innerText = 'Item & Location Hints'; + hintsDiv.appendChild(hintsHeader); + const hintsDescription = document.createElement('p'); + hintsDescription.classList.add('setting-description'); + hintsDescription.innerText = 'Choose any items or locations to begin the game with the knowledge of where those ' + + ' items are, or what those locations contain.'; + hintsDiv.appendChild(hintsDescription); + + const itemHintsContainer = document.createElement('div'); + itemHintsContainer.classList.add('hints-container'); + + // Item Hints + const itemHintsWrapper = document.createElement('div'); + itemHintsWrapper.classList.add('hints-wrapper'); + itemHintsWrapper.innerText = 'Starting Item Hints'; + + const itemHintsDiv = document.createElement('div'); + itemHintsDiv.classList.add('simple-list'); + this.data.gameItems.forEach((item) => { + const itemRow = document.createElement('div'); + itemRow.classList.add('list-row'); + + const itemLabel = document.createElement('label'); + itemLabel.setAttribute('for', `${this.name}-start_hints-${item}`); + + const itemCheckbox = document.createElement('input'); + itemCheckbox.setAttribute('type', 'checkbox'); + itemCheckbox.setAttribute('id', `${this.name}-start_hints-${item}`); + itemCheckbox.setAttribute('data-game', this.name); + itemCheckbox.setAttribute('data-setting', 'start_hints'); + itemCheckbox.setAttribute('data-option', item); + if (this.current.start_hints.includes(item)) { + itemCheckbox.setAttribute('checked', 'true'); } + itemCheckbox.addEventListener('change', (evt) => this.#updateListSetting(evt)); + itemLabel.appendChild(itemCheckbox); + + const itemName = document.createElement('span'); + itemName.innerText = item; + itemLabel.appendChild(itemName); + + itemRow.appendChild(itemLabel); + itemHintsDiv.appendChild(itemRow); }); - }); - if (Object.keys(settings.game).length === 0) { - errorMessage = 'You have not chosen a game to play!'; - } + itemHintsWrapper.appendChild(itemHintsDiv); + itemHintsContainer.appendChild(itemHintsWrapper); - // Remove weights if there is only one game - else if (Object.keys(settings.game).length === 1) { - settings.game = Object.keys(settings.game)[0]; - } + // Starting Location Hints + const locationHintsWrapper = document.createElement('div'); + locationHintsWrapper.classList.add('hints-wrapper'); + locationHintsWrapper.innerText = 'Starting Location Hints'; - // If an error occurred, alert the user and do not export the file - if (errorMessage) { - userMessage.innerText = errorMessage; - userMessage.classList.add('visible'); - userMessage.scrollIntoView({ - behavior: 'smooth', - block: 'start', + const locationHintsDiv = document.createElement('div'); + locationHintsDiv.classList.add('simple-list'); + this.data.gameLocations.forEach((location) => { + const locationRow = document.createElement('div'); + locationRow.classList.add('list-row'); + + const locationLabel = document.createElement('label'); + locationLabel.setAttribute('for', `${this.name}-start_location_hints-${location}`); + + const locationCheckbox = document.createElement('input'); + locationCheckbox.setAttribute('type', 'checkbox'); + locationCheckbox.setAttribute('id', `${this.name}-start_location_hints-${location}`); + locationCheckbox.setAttribute('data-game', this.name); + locationCheckbox.setAttribute('data-setting', 'start_location_hints'); + locationCheckbox.setAttribute('data-option', location); + if (this.current.start_location_hints.includes(location)) { + locationCheckbox.setAttribute('checked', '1'); + } + locationCheckbox.addEventListener('change', (evt) => this.#updateListSetting(evt)); + locationLabel.appendChild(locationCheckbox); + + const locationName = document.createElement('span'); + locationName.innerText = location; + locationLabel.appendChild(locationName); + + locationRow.appendChild(locationLabel); + locationHintsDiv.appendChild(locationRow); }); - return; + + locationHintsWrapper.appendChild(locationHintsDiv); + itemHintsContainer.appendChild(locationHintsWrapper); + + hintsDiv.appendChild(itemHintsContainer); + return hintsDiv; } - // If no error occurred, hide the user message if it is visible - userMessage.classList.remove('visible'); - return settings; -}; + #buildLocationsDiv() { + const locationsDiv = document.createElement('div'); + locationsDiv.classList.add('locations-div'); + const locationsHeader = document.createElement('h3'); + locationsHeader.innerText = 'Priority & Exclusion Locations'; + locationsDiv.appendChild(locationsHeader); + const locationsDescription = document.createElement('p'); + locationsDescription.classList.add('setting-description'); + locationsDescription.innerText = 'Priority locations guarantee a progression item will be placed there while ' + + 'excluded locations will not contain progression or useful items.'; + locationsDiv.appendChild(locationsDescription); -const exportSettings = () => { - const settings = validateSettings(); - if (!settings) { return; } + const locationsContainer = document.createElement('div'); + locationsContainer.classList.add('locations-container'); - const yamlText = jsyaml.safeDump(settings, { noCompatMode: true }).replaceAll(/'(\d+)':/g, (x, y) => `${y}:`); - download(`${document.getElementById('player-name').value}.yaml`, yamlText); -}; + // Priority Locations + const priorityLocationsWrapper = document.createElement('div'); + priorityLocationsWrapper.classList.add('locations-wrapper'); + priorityLocationsWrapper.innerText = 'Priority Locations'; + + const priorityLocationsDiv = document.createElement('div'); + priorityLocationsDiv.classList.add('simple-list'); + this.data.gameLocations.forEach((location) => { + const locationRow = document.createElement('div'); + locationRow.classList.add('list-row'); + + const locationLabel = document.createElement('label'); + locationLabel.setAttribute('for', `${this.name}-priority_locations-${location}`); + + const locationCheckbox = document.createElement('input'); + locationCheckbox.setAttribute('type', 'checkbox'); + locationCheckbox.setAttribute('id', `${this.name}-priority_locations-${location}`); + locationCheckbox.setAttribute('data-game', this.name); + locationCheckbox.setAttribute('data-setting', 'priority_locations'); + locationCheckbox.setAttribute('data-option', location); + if (this.current.priority_locations.includes(location)) { + locationCheckbox.setAttribute('checked', '1'); + } + locationCheckbox.addEventListener('change', (evt) => this.#updateListSetting(evt)); + locationLabel.appendChild(locationCheckbox); + + const locationName = document.createElement('span'); + locationName.innerText = location; + locationLabel.appendChild(locationName); + + locationRow.appendChild(locationLabel); + priorityLocationsDiv.appendChild(locationRow); + }); + + priorityLocationsWrapper.appendChild(priorityLocationsDiv); + locationsContainer.appendChild(priorityLocationsWrapper); + + // Exclude Locations + const excludeLocationsWrapper = document.createElement('div'); + excludeLocationsWrapper.classList.add('locations-wrapper'); + excludeLocationsWrapper.innerText = 'Exclude Locations'; + + const excludeLocationsDiv = document.createElement('div'); + excludeLocationsDiv.classList.add('simple-list'); + this.data.gameLocations.forEach((location) => { + const locationRow = document.createElement('div'); + locationRow.classList.add('list-row'); + + const locationLabel = document.createElement('label'); + locationLabel.setAttribute('for', `${this.name}-exclude_locations-${location}`); + + const locationCheckbox = document.createElement('input'); + locationCheckbox.setAttribute('type', 'checkbox'); + locationCheckbox.setAttribute('id', `${this.name}-exclude_locations-${location}`); + locationCheckbox.setAttribute('data-game', this.name); + locationCheckbox.setAttribute('data-setting', 'exclude_locations'); + locationCheckbox.setAttribute('data-option', location); + if (this.current.exclude_locations.includes(location)) { + locationCheckbox.setAttribute('checked', '1'); + } + locationCheckbox.addEventListener('change', (evt) => this.#updateListSetting(evt)); + locationLabel.appendChild(locationCheckbox); + + const locationName = document.createElement('span'); + locationName.innerText = location; + locationLabel.appendChild(locationName); + + locationRow.appendChild(locationLabel); + excludeLocationsDiv.appendChild(locationRow); + }); + + excludeLocationsWrapper.appendChild(excludeLocationsDiv); + locationsContainer.appendChild(excludeLocationsWrapper); + + locationsDiv.appendChild(locationsContainer); + return locationsDiv; + } + + #updateRangeSetting(evt) { + const setting = evt.target.getAttribute('data-setting'); + const option = evt.target.getAttribute('data-option'); + document.getElementById(`${this.name}-${setting}-${option}`).innerText = evt.target.value; + if (evt.action && evt.action === 'rangeDelete') { + delete this.current[setting][option]; + } else { + this.current[setting][option] = parseInt(evt.target.value, 10); + } + this.save(); + } + + #updateListSetting(evt) { + const setting = evt.target.getAttribute('data-setting'); + const option = evt.target.getAttribute('data-option'); + + if (evt.target.checked) { + // If the option is to be enabled and it is already enabled, do nothing + if (this.current[setting].includes(option)) { return; } + + this.current[setting].push(option); + } else { + // If the option is to be disabled and it is already disabled, do nothing + if (!this.current[setting].includes(option)) { return; } + + this.current[setting].splice(this.current[setting].indexOf(option), 1); + } + this.save(); + } + + #updateItemSetting(evt) { + const setting = evt.target.getAttribute('data-setting'); + const option = evt.target.getAttribute('data-option'); + if (setting === 'start_inventory') { + this.current[setting][option] = evt.target.value.trim() ? parseInt(evt.target.value) : 0; + } else { + this.current[setting][option] = isNaN(evt.target.value) ? + evt.target.value : parseInt(evt.target.value, 10); + } + this.save(); + } + + // Saves the current settings to local storage. + save() { + this.#allSettings.save(); + } +} /** Create an anchor and trigger a download of a text file. */ const download = (filename, text) => { @@ -1223,30 +1282,3 @@ const download = (filename, text) => { downloadLink.click(); document.body.removeChild(downloadLink); }; - -const generateGame = (raceMode = false) => { - const settings = validateSettings(); - if (!settings) { return; } - - axios.post('/api/generate', { - weights: { player: JSON.stringify(settings) }, - presetData: { player: JSON.stringify(settings) }, - playerCount: 1, - spoiler: 3, - race: raceMode ? '1' : '0', - }).then((response) => { - window.location.href = response.data.url; - }).catch((error) => { - const userMessage = document.getElementById('user-message'); - userMessage.innerText = 'Something went wrong and your game could not be generated.'; - if (error.response.data.text) { - userMessage.innerText += ' ' + error.response.data.text; - } - userMessage.classList.add('visible'); - userMessage.scrollIntoView({ - behavior: 'smooth', - block: 'start', - }); - console.error(error); - }); -}; From b707619aad6bf557559ad0a619cf716c51676747 Mon Sep 17 00:00:00 2001 From: Bryce Wilson Date: Wed, 18 Oct 2023 22:07:15 -0700 Subject: [PATCH 16/54] BizHawkClient: Add autostart setting (#2322) --- settings.py | 20 ++++++++++++++++++++ worlds/_bizhawk/context.py | 21 +++++++++++++++++++-- 2 files changed, 39 insertions(+), 2 deletions(-) diff --git a/settings.py b/settings.py index a7dcbbf8dd..acae86095c 100644 --- a/settings.py +++ b/settings.py @@ -694,6 +694,25 @@ does nothing if not found snes_rom_start: Union[SnesRomStart, bool] = True +class BizHawkClientOptions(Group): + class EmuHawkPath(UserFilePath): + """ + The location of the EmuHawk you want to auto launch patched ROMs with + """ + is_exe = True + description = "EmuHawk Executable" + + class RomStart(str): + """ + Set this to true to autostart a patched ROM in BizHawk with the connector script, + to false to never open the patched rom automatically, + or to a path to an external program to open the ROM file with that instead. + """ + + emuhawk_path: EmuHawkPath = EmuHawkPath(None) + rom_start: Union[RomStart, bool] = True + + # Top-level group with lazy loading of worlds class Settings(Group): @@ -701,6 +720,7 @@ class Settings(Group): server_options: ServerOptions = ServerOptions() generator: GeneratorOptions = GeneratorOptions() sni_options: SNIOptions = SNIOptions() + bizhawkclient_options: BizHawkClientOptions = BizHawkClientOptions() _filename: Optional[str] = None diff --git a/worlds/_bizhawk/context.py b/worlds/_bizhawk/context.py index 6e53b370af..465334274e 100644 --- a/worlds/_bizhawk/context.py +++ b/worlds/_bizhawk/context.py @@ -5,6 +5,7 @@ checking or launching the client, otherwise it will probably cause circular impo import asyncio +import subprocess import traceback from typing import Any, Dict, Optional @@ -146,8 +147,24 @@ async def _game_watcher(ctx: BizHawkClientContext): async def _run_game(rom: str): - import webbrowser - webbrowser.open(rom) + import os + auto_start = Utils.get_settings().bizhawkclient_options.rom_start + + if auto_start is True: + emuhawk_path = Utils.get_settings().bizhawkclient_options.emuhawk_path + subprocess.Popen([emuhawk_path, "--lua=data/lua/connector_bizhawk_generic.lua", os.path.realpath(rom)], + cwd=Utils.local_path("."), + stdin=subprocess.DEVNULL, + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL) + elif isinstance(auto_start, str): + import shlex + + subprocess.Popen([*shlex.split(auto_start), os.path.realpath(rom)], + cwd=Utils.local_path("."), + stdin=subprocess.DEVNULL, + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL) async def _patch_and_run_game(patch_file: str): From fb6b66463da5235603abe01aa3110098376ce17d Mon Sep 17 00:00:00 2001 From: Aaron Wagener Date: Thu, 19 Oct 2023 18:36:18 -0500 Subject: [PATCH 17/54] OC2: fix mistakes when moving to new options api (#2332) --- worlds/overcooked2/__init__.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/worlds/overcooked2/__init__.py b/worlds/overcooked2/__init__.py index 2bf523b347..0451f32bdd 100644 --- a/worlds/overcooked2/__init__.py +++ b/worlds/overcooked2/__init__.py @@ -172,7 +172,7 @@ class Overcooked2World(World): # random priority locations have no desirable effect on solo seeds return list() - balancing_mode = self.get_options()["LocationBalancing"] + balancing_mode = self.options.location_balancing if balancing_mode == LocationBalancingMode.disabled: # Location balancing is disabled, progression density is purely determined by filler @@ -528,7 +528,7 @@ class Overcooked2World(World): # Game Modifications "LevelPurchaseRequirements": level_purchase_requirements, "Custom66TimerScale": max(0.4, 0.25 + (1.0 - star_threshold_scale)*0.6), - "ShortHordeLevels": self.options.short_horde_levels, + "ShortHordeLevels": self.options.short_horde_levels.result, "CustomLevelOrder": custom_level_order, # Items (Starting Inventory) @@ -584,6 +584,7 @@ class Overcooked2World(World): "TwoStars": star_threshold_scale * 0.75, "OneStar": star_threshold_scale * 0.35, } + base_data["AlwaysServeOldestOrder"] = self.options.always_serve_oldest_order.result return base_data From 385803eb5ce3b9df4ca5d9307e0d2046be7b3854 Mon Sep 17 00:00:00 2001 From: Justus Lind Date: Fri, 20 Oct 2023 10:13:17 +1000 Subject: [PATCH 18/54] Muse Dash: Add support for specifying specific DLCs (#2329) --- worlds/musedash/Items.py | 2 +- worlds/musedash/MuseDashCollection.py | 38 ++++++-- worlds/musedash/MuseDashData.txt | 94 ++++++++++++-------- worlds/musedash/Options.py | 52 ++++++----- worlds/musedash/__init__.py | 59 ++++++------ worlds/musedash/test/TestCollection.py | 17 +++- worlds/musedash/test/TestDifficultyRanges.py | 3 +- 7 files changed, 166 insertions(+), 99 deletions(-) diff --git a/worlds/musedash/Items.py b/worlds/musedash/Items.py index be229228bd..63fd3aa51b 100644 --- a/worlds/musedash/Items.py +++ b/worlds/musedash/Items.py @@ -6,7 +6,7 @@ class SongData(NamedTuple): """Special data container to contain the metadata of each song to make filtering work.""" code: Optional[int] - song_is_free: bool + album: str streamer_mode: bool easy: Optional[int] hard: Optional[int] diff --git a/worlds/musedash/MuseDashCollection.py b/worlds/musedash/MuseDashCollection.py index 7812e28b7a..1807dce2f9 100644 --- a/worlds/musedash/MuseDashCollection.py +++ b/worlds/musedash/MuseDashCollection.py @@ -1,5 +1,5 @@ from .Items import SongData, AlbumData -from typing import Dict, List, Optional +from typing import Dict, List, Set, Optional from collections import ChainMap @@ -15,13 +15,21 @@ class MuseDashCollections: MUSIC_SHEET_NAME: str = "Music Sheet" MUSIC_SHEET_CODE: int = STARTING_CODE - FREE_ALBUMS = [ + FREE_ALBUMS: List[str] = [ "Default Music", "Budget Is Burning: Nano Core", "Budget Is Burning Vol.1", ] - DIFF_OVERRIDES = [ + MUSE_PLUS_DLC: str = "Muse Plus" + DLC: List[str] = [ + # MUSE_PLUS_DLC, # To be included when OptionSets are rendered as part of basic settings. + # "maimai DX Limited-time Suite", # Part of Muse Plus. Goes away 31st Jan 2026. + "Miku in Museland", # Paid DLC not included in Muse Plus + "MSR Anthology", # Part of Muse Plus. Goes away 20th Jan 2024. + ] + + DIFF_OVERRIDES: List[str] = [ "MuseDash ka nanika hi", "Rush-Hour", "Find this Month's Featured Playlist", @@ -48,8 +56,8 @@ class MuseDashCollections: "Error SFX Trap": STARTING_CODE + 9, } - item_names_to_id = ChainMap({}, sfx_trap_items, vfx_trap_items) - location_names_to_id = ChainMap(song_locations, album_locations) + item_names_to_id: ChainMap = ChainMap({}, sfx_trap_items, vfx_trap_items) + location_names_to_id: ChainMap = ChainMap(song_locations, album_locations) def __init__(self) -> None: self.item_names_to_id[self.MUSIC_SHEET_NAME] = self.MUSIC_SHEET_CODE @@ -70,7 +78,6 @@ class MuseDashCollections: # Data is in the format 'Song|UID|Album|StreamerMode|EasyDiff|HardDiff|MasterDiff|SecretDiff' song_name = sections[0] # [1] is used in the client copy to make sure item id's match. - song_is_free = album in self.FREE_ALBUMS steamer_mode = sections[3] == "True" if song_name in self.DIFF_OVERRIDES: @@ -84,7 +91,7 @@ class MuseDashCollections: diff_of_hard = self.parse_song_difficulty(sections[5]) diff_of_master = self.parse_song_difficulty(sections[6]) - self.song_items[song_name] = SongData(item_id_index, song_is_free, steamer_mode, + self.song_items[song_name] = SongData(item_id_index, album, steamer_mode, diff_of_easy, diff_of_hard, diff_of_master) item_id_index += 1 @@ -102,13 +109,13 @@ class MuseDashCollections: self.song_locations[f"{name}-1"] = location_id_index + 1 location_id_index += 2 - def get_songs_with_settings(self, dlc_songs: bool, streamer_mode_active: bool, + def get_songs_with_settings(self, dlc_songs: Set[str], streamer_mode_active: bool, diff_lower: int, diff_higher: int) -> List[str]: """Gets a list of all songs that match the filter settings. Difficulty thresholds are inclusive.""" filtered_list = [] for songKey, songData in self.song_items.items(): - if not dlc_songs and not songData.song_is_free: + if not self.song_matches_dlc_filter(songData, dlc_songs): continue if streamer_mode_active and not songData.streamer_mode: @@ -128,6 +135,19 @@ class MuseDashCollections: return filtered_list + def song_matches_dlc_filter(self, song: SongData, dlc_songs: Set[str]) -> bool: + if song.album in self.FREE_ALBUMS: + return True + + if song.album in dlc_songs: + return True + + # Muse Plus provides access to any DLC not included as a seperate pack + if song.album not in self.DLC and self.MUSE_PLUS_DLC in dlc_songs: + return True + + return False + def parse_song_difficulty(self, difficulty: str) -> Optional[int]: """Attempts to parse the song difficulty.""" if len(difficulty) <= 0 or difficulty == "?" or difficulty == "¿": diff --git a/worlds/musedash/MuseDashData.txt b/worlds/musedash/MuseDashData.txt index 8d6c3f3753..bd07fef7af 100644 --- a/worlds/musedash/MuseDashData.txt +++ b/worlds/musedash/MuseDashData.txt @@ -51,42 +51,42 @@ Mujinku-Vacuum|0-28|Default Music|False|5|7|11| MilK|0-36|Default Music|False|5|7|9| umpopoff|0-41|Default Music|False|0|?|0| Mopemope|0-45|Default Music|False|4|7|9|11 -The Happycore Idol|43-0|Just as Planned Plus|True|2|5|7| -Amatsumikaboshi|43-1|Just as Planned Plus|True|4|6|8|10 -ARIGA THESIS|43-2|Just as Planned Plus|True|3|6|10| -Night of Nights|43-3|Just as Planned Plus|False|4|7|10| -#Psychedelic_Meguro_River|43-4|Just as Planned Plus|False|3|6|8| -can you feel it|43-5|Just as Planned Plus|False|4|6|8|9 -Midnight O'clock|43-6|Just as Planned Plus|True|3|6|8| -Rin|43-7|Just as Planned Plus|True|5|7|10| -Smile-mileS|43-8|Just as Planned Plus|False|6|8|10| -Believing and Being|43-9|Just as Planned Plus|True|4|6|9| -Catalyst|43-10|Just as Planned Plus|False|5|7|9| -don't!stop!eroero!|43-11|Just as Planned Plus|True|5|7|9| -pa pi pu pi pu pi pa|43-12|Just as Planned Plus|False|6|8|10| -Sand Maze|43-13|Just as Planned Plus|True|6|8|10|11 -Diffraction|43-14|Just as Planned Plus|True|5|8|10| -AKUMU|43-15|Just as Planned Plus|False|4|6|8| -Queen Aluett|43-16|Just as Planned Plus|True|7|9|11| -DROPS|43-17|Just as Planned Plus|False|2|5|8| -Frightfully-insane Flan-chan's frightful song|43-18|Just as Planned Plus|False|5|7|10| -snooze|43-19|Just as Planned Plus|False|5|7|10| -Kuishinbo Hacker feat.Kuishinbo Akachan|43-20|Just as Planned Plus|True|5|7|9| -Inu no outa|43-21|Just as Planned Plus|True|3|5|7| -Prism Fountain|43-22|Just as Planned Plus|True|7|9|11| -Gospel|43-23|Just as Planned Plus|False|4|6|9| +The Happycore Idol|43-0|MD Plus Project|True|2|5|7| +Amatsumikaboshi|43-1|MD Plus Project|True|4|6|8|10 +ARIGA THESIS|43-2|MD Plus Project|True|3|6|10| +Night of Nights|43-3|MD Plus Project|False|4|7|10| +#Psychedelic_Meguro_River|43-4|MD Plus Project|False|3|6|8| +can you feel it|43-5|MD Plus Project|False|4|6|8|9 +Midnight O'clock|43-6|MD Plus Project|True|3|6|8| +Rin|43-7|MD Plus Project|True|5|7|10| +Smile-mileS|43-8|MD Plus Project|False|6|8|10| +Believing and Being|43-9|MD Plus Project|True|4|6|9| +Catalyst|43-10|MD Plus Project|False|5|7|9| +don't!stop!eroero!|43-11|MD Plus Project|True|5|7|9| +pa pi pu pi pu pi pa|43-12|MD Plus Project|False|6|8|10| +Sand Maze|43-13|MD Plus Project|True|6|8|10|11 +Diffraction|43-14|MD Plus Project|True|5|8|10| +AKUMU|43-15|MD Plus Project|False|4|6|8| +Queen Aluett|43-16|MD Plus Project|True|7|9|11| +DROPS|43-17|MD Plus Project|False|2|5|8| +Frightfully-insane Flan-chan's frightful song|43-18|MD Plus Project|False|5|7|10| +snooze|43-19|MD Plus Project|False|5|7|10| +Kuishinbo Hacker feat.Kuishinbo Akachan|43-20|MD Plus Project|True|5|7|9| +Inu no outa|43-21|MD Plus Project|True|3|5|7| +Prism Fountain|43-22|MD Plus Project|True|7|9|11| +Gospel|43-23|MD Plus Project|False|4|6|9| East Ai Li Lovely|62-0|Happy Otaku Pack Vol.17|False|2|4|7| Mori Umi no Fune|62-1|Happy Otaku Pack Vol.17|True|5|7|9| Ooi|62-2|Happy Otaku Pack Vol.17|True|5|7|10| Numatta!!|62-3|Happy Otaku Pack Vol.17|True|5|7|9| -SATELLITE|62-4|Happy Otaku Pack Vol.17|False|5|7|9| +SATELLITE|62-4|Happy Otaku Pack Vol.17|False|5|7|9|10 Fantasia Sonata Colorful feat. V!C|62-5|Happy Otaku Pack Vol.17|True|6|8|11| MuseDash ka nanika hi|61-0|Ola Dash|True|?|?|¿| Aleph-0|61-1|Ola Dash|True|7|9|11| Buttoba Supernova|61-2|Ola Dash|False|5|7|10|11 Rush-Hour|61-3|Ola Dash|False|IG|Jh|a2|Eh 3rd Avenue|61-4|Ola Dash|False|3|5|〇| -WORLDINVADER|61-5|Ola Dash|True|5|8|10| +WORLDINVADER|61-5|Ola Dash|True|5|8|10|11 N3V3R G3T OV3R|60-0|maimai DX Limited-time Suite|True|4|7|10| Oshama Scramble!|60-1|maimai DX Limited-time Suite|True|5|7|10| Valsqotch|60-2|maimai DX Limited-time Suite|True|5|9|11| @@ -450,13 +450,13 @@ Love Patrol|63-2|MUSE RADIO FM104|True|3|5|7| Mahorova|63-3|MUSE RADIO FM104|True|3|5|8| Yoru no machi|63-4|MUSE RADIO FM104|True|1|4|7| INTERNET YAMERO|63-5|MUSE RADIO FM104|True|6|8|10| -Abracadabra|43-24|Just as Planned Plus|False|6|8|10| -Squalldecimator feat. EZ-Ven|43-25|Just as Planned Plus|True|5|7|9| -Amateras Rhythm|43-26|Just as Planned Plus|True|6|8|11| -Record one's Dream|43-27|Just as Planned Plus|False|4|7|10| -Lunatic|43-28|Just as Planned Plus|True|5|8|10| -Jiumeng|43-29|Just as Planned Plus|True|3|6|8| -The Day We Become Family|43-30|Just as Planned Plus|True|3|5|8| +Abracadabra|43-24|MD Plus Project|False|6|8|10| +Squalldecimator feat. EZ-Ven|43-25|MD Plus Project|True|5|7|9| +Amateras Rhythm|43-26|MD Plus Project|True|6|8|11| +Record one's Dream|43-27|MD Plus Project|False|4|7|10| +Lunatic|43-28|MD Plus Project|True|5|8|10| +Jiumeng|43-29|MD Plus Project|True|3|6|8| +The Day We Become Family|43-30|MD Plus Project|True|3|5|8| Sutori ma FIRE!?!?|64-0|COSMIC RADIO PEROLIST|True|3|5|8| Tanuki Step|64-1|COSMIC RADIO PEROLIST|True|5|7|10|11 Space Stationery|64-2|COSMIC RADIO PEROLIST|True|5|7|10| @@ -465,7 +465,27 @@ Kawai Splendid Space Thief|64-4|COSMIC RADIO PEROLIST|False|6|8|10|11 Night City Runway|64-5|COSMIC RADIO PEROLIST|True|4|6|8| Chaos Shotgun feat. ChumuNote|64-6|COSMIC RADIO PEROLIST|True|6|8|10| mew mew magical summer|64-7|COSMIC RADIO PEROLIST|False|5|8|10|11 -BrainDance|65-0|Neon Abyss|True|3|6|9| -My Focus!|65-1|Neon Abyss|True|5|7|10| -ABABABA BURST|65-2|Neon Abyss|True|5|7|9| -ULTRA HIGHER|65-3|Neon Abyss|True|4|7|10| \ No newline at end of file +BrainDance|65-0|NeonAbyss|True|3|6|9| +My Focus!|65-1|NeonAbyss|True|5|7|10| +ABABABA BURST|65-2|NeonAbyss|True|5|7|9| +ULTRA HIGHER|65-3|NeonAbyss|True|4|7|10| +Silver Bullet|43-31|MD Plus Project|True|5|7|10| +Random|43-32|MD Plus Project|True|4|7|9| +OTOGE-BOSS-KYOKU-CHAN|43-33|MD Plus Project|False|6|8|10|11 +Crow Rabbit|43-34|MD Plus Project|True|7|9|11| +SyZyGy|43-35|MD Plus Project|True|6|8|10|11 +Mermaid Radio|43-36|MD Plus Project|True|3|5|7| +Helixir|43-37|MD Plus Project|False|6|8|10| +Highway Cruisin'|43-38|MD Plus Project|False|3|5|8| +JACK PT BOSS|43-39|MD Plus Project|False|6|8|10| +Time Capsule|43-40|MD Plus Project|False|7|9|11| +39 Music!|66-0|Miku in Museland|False|3|5|8| +Hand in Hand|66-1|Miku in Museland|False|1|3|6| +Cynical Night Plan|66-2|Miku in Museland|False|4|6|8| +God-ish|66-3|Miku in Museland|False|4|7|10| +Darling Dance|66-4|Miku in Museland|False|4|7|9| +Hatsune Creation Myth|66-5|Miku in Museland|False|6|8|10| +The Vampire|66-6|Miku in Museland|False|4|6|9| +Future Eve|66-7|Miku in Museland|False|4|8|11| +Unknown Mother Goose|66-8|Miku in Museland|False|4|8|10| +Shun-ran|66-9|Miku in Museland|False|4|7|9| \ No newline at end of file diff --git a/worlds/musedash/Options.py b/worlds/musedash/Options.py index b2f15ecc8e..3fe28187fa 100644 --- a/worlds/musedash/Options.py +++ b/worlds/musedash/Options.py @@ -1,10 +1,19 @@ from typing import Dict -from Options import Toggle, Option, Range, Choice, DeathLink, ItemSet +from Options import Toggle, Option, Range, Choice, DeathLink, ItemSet, OptionSet, PerGameCommonOptions +from dataclasses import dataclass +from .MuseDashCollection import MuseDashCollections class AllowJustAsPlannedDLCSongs(Toggle): - """Whether [Just as Planned]/[Muse Plus] DLC Songs, and all the DLCs along with it, will be included in the randomizer.""" - display_name = "Allow [Just as Planned]/[Muse Plus] DLC Songs" + """Whether [Muse Plus] DLC Songs, and all the albums included in it, can be chosen as randomised songs. + Note: The [Just As Planned] DLC contains all [Muse Plus] songs.""" + display_name = "Allow [Muse Plus] DLC Songs" + +class DLCMusicPacks(OptionSet): + """Which non-[Muse Plus] DLC packs can be chosen as randomised songs.""" + display_name = "DLC Packs" + default = {} + valid_keys = [dlc for dlc in MuseDashCollections.DLC] class StreamerModeEnabled(Toggle): @@ -159,21 +168,22 @@ class ExcludeSongs(ItemSet): display_name = "Exclude Songs" -musedash_options: Dict[str, type(Option)] = { - "allow_just_as_planned_dlc_songs": AllowJustAsPlannedDLCSongs, - "streamer_mode_enabled": StreamerModeEnabled, - "starting_song_count": StartingSongs, - "additional_song_count": AdditionalSongs, - "additional_item_percentage": AdditionalItemPercentage, - "song_difficulty_mode": DifficultyMode, - "song_difficulty_min": DifficultyModeOverrideMin, - "song_difficulty_max": DifficultyModeOverrideMax, - "grade_needed": GradeNeeded, - "music_sheet_count_percentage": MusicSheetCountPercentage, - "music_sheet_win_count_percentage": MusicSheetWinCountPercentage, - "available_trap_types": TrapTypes, - "trap_count_percentage": TrapCountPercentage, - "death_link": DeathLink, - "include_songs": IncludeSongs, - "exclude_songs": ExcludeSongs -} +@dataclass +class MuseDashOptions(PerGameCommonOptions): + allow_just_as_planned_dlc_songs: AllowJustAsPlannedDLCSongs + dlc_packs: DLCMusicPacks + streamer_mode_enabled: StreamerModeEnabled + starting_song_count: StartingSongs + additional_song_count: AdditionalSongs + additional_item_percentage: AdditionalItemPercentage + song_difficulty_mode: DifficultyMode + song_difficulty_min: DifficultyModeOverrideMin + song_difficulty_max: DifficultyModeOverrideMax + grade_needed: GradeNeeded + music_sheet_count_percentage: MusicSheetCountPercentage + music_sheet_win_count_percentage: MusicSheetWinCountPercentage + available_trap_types: TrapTypes + trap_count_percentage: TrapCountPercentage + death_link: DeathLink + include_songs: IncludeSongs + exclude_songs: ExcludeSongs diff --git a/worlds/musedash/__init__.py b/worlds/musedash/__init__.py index 754d2352e0..63ce123c93 100644 --- a/worlds/musedash/__init__.py +++ b/worlds/musedash/__init__.py @@ -1,10 +1,10 @@ from worlds.AutoWorld import World, WebWorld -from worlds.generic.Rules import set_rule from BaseClasses import Region, Item, ItemClassification, Entrance, Tutorial -from typing import List +from typing import List, ClassVar, Type from math import floor +from Options import PerGameCommonOptions -from .Options import musedash_options +from .Options import MuseDashOptions from .Items import MuseDashSongItem, MuseDashFixedItem from .Locations import MuseDashLocation from .MuseDashCollection import MuseDashCollections @@ -47,9 +47,9 @@ class MuseDashWorld(World): # World Options game = "Muse Dash" - option_definitions = musedash_options + options_dataclass: ClassVar[Type[PerGameCommonOptions]] = MuseDashOptions topology_present = False - data_version = 9 + data_version = 10 web = MuseDashWebWorld() # Necessary Data @@ -66,14 +66,17 @@ class MuseDashWorld(World): location_count: int def generate_early(self): - dlc_songs = self.multiworld.allow_just_as_planned_dlc_songs[self.player] - streamer_mode = self.multiworld.streamer_mode_enabled[self.player] + dlc_songs = {key for key in self.options.dlc_packs.value} + if (self.options.allow_just_as_planned_dlc_songs.value): + dlc_songs.add(self.md_collection.MUSE_PLUS_DLC) + + streamer_mode = self.options.streamer_mode_enabled (lower_diff_threshold, higher_diff_threshold) = self.get_difficulty_range() # The minimum amount of songs to make an ok rando would be Starting Songs + 10 interim songs + Goal song. # - Interim songs being equal to max starting song count. # Note: The worst settings still allow 25 songs (Streamer Mode + No DLC). - starter_song_count = self.multiworld.starting_song_count[self.player].value + starter_song_count = self.options.starting_song_count.value while True: # In most cases this should only need to run once @@ -104,9 +107,9 @@ class MuseDashWorld(World): def handle_plando(self, available_song_keys: List[str]) -> List[str]: song_items = self.md_collection.song_items - start_items = self.multiworld.start_inventory[self.player].value.keys() - include_songs = self.multiworld.include_songs[self.player].value - exclude_songs = self.multiworld.exclude_songs[self.player].value + start_items = self.options.start_inventory.value.keys() + include_songs = self.options.include_songs.value + exclude_songs = self.options.exclude_songs.value self.starting_songs = [s for s in start_items if s in song_items] self.included_songs = [s for s in include_songs if s in song_items and s not in self.starting_songs] @@ -115,8 +118,8 @@ class MuseDashWorld(World): and s not in include_songs and s not in exclude_songs] def create_song_pool(self, available_song_keys: List[str]): - starting_song_count = self.multiworld.starting_song_count[self.player].value - additional_song_count = self.multiworld.additional_song_count[self.player].value + starting_song_count = self.options.starting_song_count.value + additional_song_count = self.options.additional_song_count.value self.random.shuffle(available_song_keys) @@ -150,7 +153,7 @@ class MuseDashWorld(World): # Then attempt to fufill any remaining songs for interim songs if len(self.included_songs) < additional_song_count: - for _ in range(len(self.included_songs), self.multiworld.additional_song_count[self.player]): + for _ in range(len(self.included_songs), self.options.additional_song_count): if len(available_song_keys) <= 0: break self.included_songs.append(available_song_keys.pop()) @@ -258,40 +261,40 @@ class MuseDashWorld(World): state.has(self.md_collection.MUSIC_SHEET_NAME, self.player, self.get_music_sheet_win_count()) def get_available_traps(self) -> List[str]: - dlc_songs = self.multiworld.allow_just_as_planned_dlc_songs[self.player] + sfx_traps_available = self.options.allow_just_as_planned_dlc_songs.value trap_list = [] - if self.multiworld.available_trap_types[self.player].value & 1 != 0: + if self.options.available_trap_types.value & 1 != 0: trap_list += self.md_collection.vfx_trap_items.keys() # SFX options are only available under Just as Planned DLC. - if dlc_songs and self.multiworld.available_trap_types[self.player].value & 2 != 0: + if sfx_traps_available and self.options.available_trap_types.value & 2 != 0: trap_list += self.md_collection.sfx_trap_items.keys() return trap_list def get_additional_item_percentage(self) -> int: - trap_count = self.multiworld.trap_count_percentage[self.player].value - song_count = self.multiworld.music_sheet_count_percentage[self.player].value - return max(trap_count + song_count, self.multiworld.additional_item_percentage[self.player].value) + trap_count = self.options.trap_count_percentage.value + song_count = self.options.music_sheet_count_percentage.value + return max(trap_count + song_count, self.options.additional_item_percentage.value) def get_trap_count(self) -> int: - multiplier = self.multiworld.trap_count_percentage[self.player].value / 100.0 + multiplier = self.options.trap_count_percentage.value / 100.0 trap_count = (len(self.starting_songs) * 2) + len(self.included_songs) return max(0, floor(trap_count * multiplier)) def get_music_sheet_count(self) -> int: - multiplier = self.multiworld.music_sheet_count_percentage[self.player].value / 100.0 + multiplier = self.options.music_sheet_count_percentage.value / 100.0 song_count = (len(self.starting_songs) * 2) + len(self.included_songs) return max(1, floor(song_count * multiplier)) def get_music_sheet_win_count(self) -> int: - multiplier = self.multiworld.music_sheet_win_count_percentage[self.player].value / 100.0 + multiplier = self.options.music_sheet_win_count_percentage.value / 100.0 sheet_count = self.get_music_sheet_count() return max(1, floor(sheet_count * multiplier)) def get_difficulty_range(self) -> List[int]: - difficulty_mode = self.multiworld.song_difficulty_mode[self.player] + difficulty_mode = self.options.song_difficulty_mode # Valid difficulties are between 1 and 11. But make it 0 to 12 for safety difficulty_bounds = [0, 12] @@ -309,8 +312,8 @@ class MuseDashWorld(World): elif difficulty_mode == 5: difficulty_bounds[0] = 10 elif difficulty_mode == 6: - minimum_difficulty = self.multiworld.song_difficulty_min[self.player].value - maximum_difficulty = self.multiworld.song_difficulty_max[self.player].value + minimum_difficulty = self.options.song_difficulty_min.value + maximum_difficulty = self.options.song_difficulty_max.value difficulty_bounds[0] = min(minimum_difficulty, maximum_difficulty) difficulty_bounds[1] = max(minimum_difficulty, maximum_difficulty) @@ -320,7 +323,7 @@ class MuseDashWorld(World): def fill_slot_data(self): return { "victoryLocation": self.victory_song_name, - "deathLink": self.multiworld.death_link[self.player].value, + "deathLink": self.options.death_link.value, "musicSheetWinCount": self.get_music_sheet_win_count(), - "gradeNeeded": self.multiworld.grade_needed[self.player].value + "gradeNeeded": self.options.grade_needed.value } diff --git a/worlds/musedash/test/TestCollection.py b/worlds/musedash/test/TestCollection.py index 23348af104..f9422388ae 100644 --- a/worlds/musedash/test/TestCollection.py +++ b/worlds/musedash/test/TestCollection.py @@ -36,14 +36,27 @@ class CollectionsTest(unittest.TestCase): def test_free_dlc_included_in_base_songs(self) -> None: collection = MuseDashCollections() - songs = collection.get_songs_with_settings(False, False, 0, 11) + songs = collection.get_songs_with_settings(set(), False, 0, 12) self.assertIn("Glimmer", songs, "Budget Is Burning Vol.1 is not being included in base songs") self.assertIn("Out of Sense", songs, "Budget Is Burning: Nano Core is not being included in base songs") + def test_dlcs(self) -> None: + collection = MuseDashCollections() + free_song_count = len(collection.get_songs_with_settings(set(), False, 0, 12)) + known_mp_song = "The Happycore Idol" + + for dlc in collection.DLC: + songs_with_dlc = collection.get_songs_with_settings({dlc}, False, 0, 12) + self.assertGreater(len(songs_with_dlc), free_song_count, f"DLC {dlc} did not include extra songs.") + if dlc == collection.MUSE_PLUS_DLC: + self.assertIn(known_mp_song, songs_with_dlc, f"Muse Plus missing muse plus song.") + else: + self.assertNotIn(known_mp_song, songs_with_dlc, f"DLC {dlc} includes Muse Plus songs.") + def test_remove_songs_are_not_generated(self) -> None: collection = MuseDashCollections() - songs = collection.get_songs_with_settings(True, False, 0, 11) + songs = collection.get_songs_with_settings({x for x in collection.DLC}, False, 0, 12) for song_name in self.REMOVED_SONGS: self.assertNotIn(song_name, songs, f"Song '{song_name}' wasn't removed correctly.") diff --git a/worlds/musedash/test/TestDifficultyRanges.py b/worlds/musedash/test/TestDifficultyRanges.py index 58817d0fc3..01420347af 100644 --- a/worlds/musedash/test/TestDifficultyRanges.py +++ b/worlds/musedash/test/TestDifficultyRanges.py @@ -4,6 +4,7 @@ from . import MuseDashTestBase class DifficultyRanges(MuseDashTestBase): def test_all_difficulty_ranges(self) -> None: muse_dash_world = self.multiworld.worlds[1] + dlc_set = {x for x in muse_dash_world.md_collection.DLC} difficulty_choice = self.multiworld.song_difficulty_mode[1] difficulty_min = self.multiworld.song_difficulty_min[1] difficulty_max = self.multiworld.song_difficulty_max[1] @@ -12,7 +13,7 @@ class DifficultyRanges(MuseDashTestBase): self.assertEqual(inputRange[0], lower) self.assertEqual(inputRange[1], upper) - songs = muse_dash_world.md_collection.get_songs_with_settings(True, False, inputRange[0], inputRange[1]) + songs = muse_dash_world.md_collection.get_songs_with_settings(dlc_set, False, inputRange[0], inputRange[1]) for songKey in songs: song = muse_dash_world.md_collection.song_items[songKey] if (song.easy is not None and inputRange[0] <= song.easy <= inputRange[1]): From b82f48fe4b408ccfd66c9eb3f1eaefda19fb665b Mon Sep 17 00:00:00 2001 From: espeon65536 <81029175+espeon65536@users.noreply.github.com> Date: Thu, 19 Oct 2023 18:23:32 -0600 Subject: [PATCH 19/54] Core: guard against plandoing items onto event locations (#2284) --- Fill.py | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/Fill.py b/Fill.py index 600d18ef2a..94528a307f 100644 --- a/Fill.py +++ b/Fill.py @@ -897,19 +897,22 @@ def distribute_planned(world: MultiWorld) -> None: for item_name in items: item = world.worlds[player].create_item(item_name) for location in reversed(candidates): - if not location.item: - if location.item_rule(item): - if location.can_fill(world.state, item, False): - successful_pairs.append((item, location)) - candidates.remove(location) - count = count + 1 - break + if (location.address is None) == (item.code is None): # either both None or both not None + if not location.item: + if location.item_rule(item): + if location.can_fill(world.state, item, False): + successful_pairs.append((item, location)) + candidates.remove(location) + count = count + 1 + break + else: + err.append(f"Can't place item at {location} due to fill condition not met.") else: - err.append(f"Can't place item at {location} due to fill condition not met.") + err.append(f"{item_name} not allowed at {location}.") else: - err.append(f"{item_name} not allowed at {location}.") + err.append(f"Cannot place {item_name} into already filled location {location}.") else: - err.append(f"Cannot place {item_name} into already filled location {location}.") + err.append(f"Mismatch between {item_name} and {location}, only one is an event.") if count == maxcount: break if count < placement['count']['min']: From 56796b7ee8b0177c1387657152865c7909037886 Mon Sep 17 00:00:00 2001 From: black-sliver <59490463+black-sliver@users.noreply.github.com> Date: Fri, 20 Oct 2023 02:58:41 +0200 Subject: [PATCH 20/54] WebHost: minor css changes to make Supported Games page usable without js (#2266) * WebHost: minor css changes to make Supported Games page usable without js * Update JS to use querySelectorAll, remove most id attributes from elements, use relative element selectors * Hide content when clearing search bar * Remove `console.log`, remove TODO --------- Co-authored-by: Chris Wilson --- WebHostLib/static/assets/supportedGames.js | 87 ++++++++------------- WebHostLib/static/styles/supportedGames.css | 12 ++- WebHostLib/templates/supportedGames.html | 28 ++++++- 3 files changed, 67 insertions(+), 60 deletions(-) diff --git a/WebHostLib/static/assets/supportedGames.js b/WebHostLib/static/assets/supportedGames.js index 1acf0e0cc5..56eb15b5e5 100644 --- a/WebHostLib/static/assets/supportedGames.js +++ b/WebHostLib/static/assets/supportedGames.js @@ -1,51 +1,32 @@ window.addEventListener('load', () => { - const gameHeaders = document.getElementsByClassName('collapse-toggle'); - Array.from(gameHeaders).forEach((header) => { - const gameName = header.getAttribute('data-game'); - header.addEventListener('click', () => { - const gameArrow = document.getElementById(`${gameName}-arrow`); - const gameInfo = document.getElementById(gameName); - if (gameInfo.classList.contains('collapsed')) { - gameArrow.innerText = '▼'; - gameInfo.classList.remove('collapsed'); - } else { - gameArrow.innerText = '▶'; - gameInfo.classList.add('collapsed'); - } - }); - }); + // Add toggle listener to all elements with .collapse-toggle + const toggleButtons = document.querySelectorAll('.collapse-toggle'); + toggleButtons.forEach((e) => e.addEventListener('click', toggleCollapse)); // Handle game filter input const gameSearch = document.getElementById('game-search'); gameSearch.value = ''; - gameSearch.addEventListener('input', (evt) => { if (!evt.target.value.trim()) { // If input is empty, display all collapsed games - return Array.from(gameHeaders).forEach((header) => { + return toggleButtons.forEach((header) => { header.style.display = null; - const gameName = header.getAttribute('data-game'); - document.getElementById(`${gameName}-arrow`).innerText = '▶'; - document.getElementById(gameName).classList.add('collapsed'); + header.firstElementChild.innerText = '▶'; + header.nextElementSibling.classList.add('collapsed'); }); } // Loop over all the games - Array.from(gameHeaders).forEach((header) => { - const gameName = header.getAttribute('data-game'); - const gameArrow = document.getElementById(`${gameName}-arrow`); - const gameInfo = document.getElementById(gameName); - + toggleButtons.forEach((header) => { // If the game name includes the search string, display the game. If not, hide it - if (gameName.toLowerCase().includes(evt.target.value.toLowerCase())) { + if (header.getAttribute('data-game').toLowerCase().includes(evt.target.value.toLowerCase())) { header.style.display = null; - gameArrow.innerText = '▼'; - gameInfo.classList.remove('collapsed'); + header.firstElementChild.innerText = '▼'; + header.nextElementSibling.classList.remove('collapsed'); } else { - console.log(header); header.style.display = 'none'; - gameArrow.innerText = '▶'; - gameInfo.classList.add('collapsed'); + header.firstElementChild.innerText = '▶'; + header.nextElementSibling.classList.add('collapsed'); } }); }); @@ -54,30 +35,30 @@ window.addEventListener('load', () => { document.getElementById('collapse-all').addEventListener('click', collapseAll); }); -const expandAll = () => { - const gameHeaders = document.getElementsByClassName('collapse-toggle'); - // Loop over all the games - Array.from(gameHeaders).forEach((header) => { - const gameName = header.getAttribute('data-game'); - const gameArrow = document.getElementById(`${gameName}-arrow`); - const gameInfo = document.getElementById(gameName); +const toggleCollapse = (evt) => { + const gameArrow = evt.target.firstElementChild; + const gameInfo = evt.target.nextElementSibling; + if (gameInfo.classList.contains('collapsed')) { + gameArrow.innerText = '▼'; + gameInfo.classList.remove('collapsed'); + } else { + gameArrow.innerText = '▶'; + gameInfo.classList.add('collapsed'); + } +}; - if (header.style.display === 'none') { return; } - gameArrow.innerText = '▼'; - gameInfo.classList.remove('collapsed'); - }); +const expandAll = () => { + document.querySelectorAll('.collapse-toggle').forEach((header) => { + if (header.style.display === 'none') { return; } + header.firstElementChild.innerText = '▼'; + header.nextElementSibling.classList.remove('collapsed'); + }); }; const collapseAll = () => { - const gameHeaders = document.getElementsByClassName('collapse-toggle'); - // Loop over all the games - Array.from(gameHeaders).forEach((header) => { - const gameName = header.getAttribute('data-game'); - const gameArrow = document.getElementById(`${gameName}-arrow`); - const gameInfo = document.getElementById(gameName); - - if (header.style.display === 'none') { return; } - gameArrow.innerText = '▶'; - gameInfo.classList.add('collapsed'); - }); + document.querySelectorAll('.collapse-toggle').forEach((header) => { + if (header.style.display === 'none') { return; } + header.firstElementChild.innerText = '▶'; + header.nextElementSibling.classList.add('collapsed'); + }); }; diff --git a/WebHostLib/static/styles/supportedGames.css b/WebHostLib/static/styles/supportedGames.css index 1e9a98c17a..7396daa954 100644 --- a/WebHostLib/static/styles/supportedGames.css +++ b/WebHostLib/static/styles/supportedGames.css @@ -18,10 +18,16 @@ margin-bottom: 2px; } +#games .collapse-toggle{ + cursor: pointer; +} + #games h2 .collapse-arrow{ font-size: 20px; + display: inline-block; /* make vertical-align work */ + padding-bottom: 9px; vertical-align: middle; - cursor: pointer; + padding-right: 8px; } #games p.collapsed{ @@ -42,12 +48,12 @@ margin-bottom: 7px; } -#games #page-controls{ +#games .page-controls{ display: flex; flex-direction: row; margin-top: 0.25rem; } -#games #page-controls button{ +#games .page-controls button{ margin-left: 0.5rem; } diff --git a/WebHostLib/templates/supportedGames.html b/WebHostLib/templates/supportedGames.html index 63b70216d7..f1514d8353 100644 --- a/WebHostLib/templates/supportedGames.html +++ b/WebHostLib/templates/supportedGames.html @@ -5,15 +5,35 @@ + {% endblock %} {% block body %} {% include 'header/oceanHeader.html' %}

Currently Supported Games

-
+

-
+
@@ -22,9 +42,9 @@ {% for game_name in worlds | title_sorted %} {% set world = worlds[game_name] %}

-  {{ game_name }} + {{ game_name }}

-

Combined File Download

+

Download

+ {% endif %}
{% endblock %} diff --git a/worlds/alttp/docs/multiworld_de.md b/worlds/alttp/docs/multiworld_de.md index 38009fb58e..8ccd1a87a6 100644 --- a/worlds/alttp/docs/multiworld_de.md +++ b/worlds/alttp/docs/multiworld_de.md @@ -67,7 +67,7 @@ Wenn du eine Option nicht gewählt haben möchtest, setze ihren Wert einfach auf ### Überprüfung deiner YAML-Datei -Wenn man sichergehen will, ob die YAML-Datei funktioniert, kann man dies bei der [YAML Validator](/mysterycheck) Seite +Wenn man sichergehen will, ob die YAML-Datei funktioniert, kann man dies bei der [YAML Validator](/check) Seite tun. ## ein Einzelspielerspiel erstellen diff --git a/worlds/alttp/docs/multiworld_es.md b/worlds/alttp/docs/multiworld_es.md index 8576318bb9..37aeda2a63 100644 --- a/worlds/alttp/docs/multiworld_es.md +++ b/worlds/alttp/docs/multiworld_es.md @@ -82,7 +82,7 @@ debe tener al menos un valor mayor que cero, si no la generación fallará. ### Verificando tu archivo YAML Si quieres validar que tu fichero YAML para asegurarte que funciona correctamente, puedes hacerlo en la pagina -[YAML Validator](/mysterycheck). +[YAML Validator](/check). ## Generar una partida para un jugador diff --git a/worlds/alttp/docs/multiworld_fr.md b/worlds/alttp/docs/multiworld_fr.md index 329ca65375..078a270f08 100644 --- a/worlds/alttp/docs/multiworld_fr.md +++ b/worlds/alttp/docs/multiworld_fr.md @@ -83,7 +83,7 @@ chaque paramètre il faut au moins une option qui soit paramétrée sur un nombr ### Vérifier son fichier YAML Si vous voulez valider votre fichier YAML pour être sûr qu'il fonctionne, vous pouvez le vérifier sur la page du -[Validateur de YAML](/mysterycheck). +[Validateur de YAML](/check). ## Générer une partie pour un joueur diff --git a/worlds/dkc3/docs/setup_en.md b/worlds/dkc3/docs/setup_en.md index bb10756300..9c4197286e 100644 --- a/worlds/dkc3/docs/setup_en.md +++ b/worlds/dkc3/docs/setup_en.md @@ -50,7 +50,7 @@ them. Player settings page: [Donkey Kong Country 3 Player Settings Page](/games/ ### Verifying your config file If you would like to validate your config file to make sure it works, you may do so on the YAML Validator page. YAML -validator page: [YAML Validation page](/mysterycheck) +validator page: [YAML Validation page](/check) ## Generating a Single-Player Game diff --git a/worlds/factorio/docs/setup_en.md b/worlds/factorio/docs/setup_en.md index 09ad431a21..b6d4545925 100644 --- a/worlds/factorio/docs/setup_en.md +++ b/worlds/factorio/docs/setup_en.md @@ -31,7 +31,7 @@ them. Factorio player settings page: [Factorio Settings Page](/games/Factorio/pl ### Verifying your config file If you would like to validate your config file to make sure it works, you may do so on the YAML Validator page. YAML -Validator page: [Yaml Validation Page](/mysterycheck) +Validator page: [Yaml Validation Page](/check) ## Connecting to Someone Else's Factorio Game diff --git a/worlds/generic/docs/setup_en.md b/worlds/generic/docs/setup_en.md index 132b88e285..93ae217e0d 100644 --- a/worlds/generic/docs/setup_en.md +++ b/worlds/generic/docs/setup_en.md @@ -40,7 +40,7 @@ game you will be playing as well as the settings you would like for that game. YAML is a format very similar to JSON however it is made to be more human-readable. If you are ever unsure of the validity of your YAML file you may check the file by uploading it to the check page on the Archipelago website: -[YAML Validation Page](/mysterycheck) +[YAML Validation Page](/check) ### Creating a YAML diff --git a/worlds/ladx/docs/setup_en.md b/worlds/ladx/docs/setup_en.md index 538d70d45e..e21c5bddc4 100644 --- a/worlds/ladx/docs/setup_en.md +++ b/worlds/ladx/docs/setup_en.md @@ -40,7 +40,7 @@ your personal settings and export a config file from them. ### Verifying your config file If you would like to validate your config file to make sure it works, you may do so on the -[YAML Validator](/mysterycheck) page. +[YAML Validator](/check) page. ## Generating a Single-Player Game diff --git a/worlds/lufia2ac/docs/setup_en.md b/worlds/lufia2ac/docs/setup_en.md index 4236c26e8a..3762f32fb4 100644 --- a/worlds/lufia2ac/docs/setup_en.md +++ b/worlds/lufia2ac/docs/setup_en.md @@ -44,7 +44,7 @@ your personal settings and export a config file from them. ### Verifying your config file If you would like to validate your config file to make sure it works, you may do so on the -[YAML Validator](/mysterycheck) page. +[YAML Validator](/check) page. ## Generating a Single-Player Game diff --git a/worlds/sm/docs/multiworld_en.md b/worlds/sm/docs/multiworld_en.md index ce91e7a7e4..1291507743 100644 --- a/worlds/sm/docs/multiworld_en.md +++ b/worlds/sm/docs/multiworld_en.md @@ -49,7 +49,7 @@ them. Player settings page: [Super Metroid Player Settings Page](/games/Super%20 ### Verifying your config file If you would like to validate your config file to make sure it works, you may do so on the YAML Validator page. YAML -validator page: [YAML Validation page](/mysterycheck) +validator page: [YAML Validation page](/check) ## Generating a Single-Player Game diff --git a/worlds/smw/docs/setup_en.md b/worlds/smw/docs/setup_en.md index 9ca8bdf58a..3967f544a0 100644 --- a/worlds/smw/docs/setup_en.md +++ b/worlds/smw/docs/setup_en.md @@ -50,7 +50,7 @@ them. Player settings page: [Super Mario World Player Settings Page](/games/Supe ### Verifying your config file If you would like to validate your config file to make sure it works, you may do so on the YAML Validator page. YAML -validator page: [YAML Validation page](/mysterycheck) +validator page: [YAML Validation page](/check) ## Joining a MultiWorld Game diff --git a/worlds/smz3/docs/multiworld_en.md b/worlds/smz3/docs/multiworld_en.md index da6e29ab69..53842a3c6f 100644 --- a/worlds/smz3/docs/multiworld_en.md +++ b/worlds/smz3/docs/multiworld_en.md @@ -47,7 +47,7 @@ them. Player settings page: [SMZ3 Player Settings Page](/games/SMZ3/player-setti ### Verifying your config file If you would like to validate your config file to make sure it works, you may do so on the YAML Validator page. YAML -validator page: [YAML Validation page](/mysterycheck) +validator page: [YAML Validation page](/check) ## Generating a Single-Player Game diff --git a/worlds/soe/docs/multiworld_en.md b/worlds/soe/docs/multiworld_en.md index d995cea56a..58b9aabf6a 100644 --- a/worlds/soe/docs/multiworld_en.md +++ b/worlds/soe/docs/multiworld_en.md @@ -29,7 +29,7 @@ them. Player settings page: [Secret of Evermore Player Settings PAge](/games/Sec ### Verifying your config file If you would like to validate your config file to make sure it works, you may do so on the YAML Validator -page: [YAML Validation page](/mysterycheck) +page: [YAML Validation page](/check) ## Generating a Single-Player Game diff --git a/worlds/tloz/docs/multiworld_en.md b/worlds/tloz/docs/multiworld_en.md index 581e8cf7b2..ae53d953b1 100644 --- a/worlds/tloz/docs/multiworld_en.md +++ b/worlds/tloz/docs/multiworld_en.md @@ -44,7 +44,7 @@ them. Player settings page: [The Legend of Zelda Player Settings Page](/games/Th ### Verifying your config file If you would like to validate your config file to make sure it works, you may do so on the YAML Validator page. YAML -validator page: [YAML Validation page](/mysterycheck) +validator page: [YAML Validation page](/check) ## Generating a Single-Player Game diff --git a/worlds/zillion/docs/setup_en.md b/worlds/zillion/docs/setup_en.md index 16000dbe3b..22dee5ee55 100644 --- a/worlds/zillion/docs/setup_en.md +++ b/worlds/zillion/docs/setup_en.md @@ -51,7 +51,7 @@ them. ### Verifying your config file -If you would like to validate your config file to make sure it works, you may do so on the [YAML Validator page](/mysterycheck). +If you would like to validate your config file to make sure it works, you may do so on the [YAML Validator page](/check). ## Generating a Single-Player Game From 9f126ad0d070c0eb42bdf416a11775aa7d01307f Mon Sep 17 00:00:00 2001 From: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com> Date: Sun, 22 Oct 2023 06:48:06 +0200 Subject: [PATCH 26/54] The Witness: Fix random events not having the correct probabilities (#2340) --- worlds/witness/__init__.py | 2 +- worlds/witness/hints.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/worlds/witness/__init__.py b/worlds/witness/__init__.py index faaafd598b..28eaba6404 100644 --- a/worlds/witness/__init__.py +++ b/worlds/witness/__init__.py @@ -66,7 +66,7 @@ class WitnessWorld(World): def _get_slot_data(self): return { - 'seed': self.multiworld.per_slot_randoms[self.player].randint(0, 1000000), + 'seed': self.random.randrange(0, 1000000), 'victory_location': int(self.player_logic.VICTORY_LOCATION, 16), 'panelhex_to_id': self.locat.CHECK_PANELHEX_TO_ID, 'item_id_to_door_hexes': StaticWitnessItems.get_item_to_door_mappings(), diff --git a/worlds/witness/hints.py b/worlds/witness/hints.py index 5d8bd5d370..4fd0edc429 100644 --- a/worlds/witness/hints.py +++ b/worlds/witness/hints.py @@ -306,7 +306,7 @@ def make_hints(multiworld: MultiWorld, player: int, hint_amount: int): else: hints.append((f"{loc} contains {item[0]}.", item[2])) - next_random_hint_is_item = multiworld.per_slot_randoms[player].randint(0, 2) + next_random_hint_is_item = multiworld.per_slot_randoms[player].randrange(0, 2) # Moving this to the new system is in the bigger refactoring PR while len(hints) < hint_amount: if next_random_hint_is_item: From 6e6fa13e441ca9a915cfd17f50ddf6b140ab3c7d Mon Sep 17 00:00:00 2001 From: Aaron Wagener Date: Sun, 22 Oct 2023 05:12:26 -0500 Subject: [PATCH 27/54] Tests: add multiworld seed to fill subtest (#2346) --- test/TestBase.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/test/TestBase.py b/test/TestBase.py index e6fbafd95a..ca7a19815c 100644 --- a/test/TestBase.py +++ b/test/TestBase.py @@ -172,7 +172,7 @@ class WorldTestBase(unittest.TestCase): items = (items,) for item in items: self.multiworld.state.collect(item) - + def remove_by_name(self, item_names: typing.Union[str, typing.Iterable[str]]) -> typing.List[Item]: """Remove all of the items in the item pool with the given names from state""" items = self.get_items_by_name(item_names) @@ -278,7 +278,6 @@ class WorldTestBase(unittest.TestCase): def testFill(self): """Generates a multiworld and validates placements with the defined options""" - # don't run this test if accessibility is set manually if not (self.run_default_tests and self.constructed): return from Fill import distribute_items_restrictive @@ -301,8 +300,8 @@ class WorldTestBase(unittest.TestCase): state.collect(location.item, True, location) return self.multiworld.has_beaten_game(state, 1) - - with self.subTest("Game", game=self.game): + + with self.subTest("Game", game=self.game, seed=self.multiworld.seed): distribute_items_restrictive(self.multiworld) call_all(self.multiworld, "post_fill") self.assertTrue(fulfills_accessibility(), "Collected all locations, but can't beat the game.") From 30da81c39043befff8c829fe816e10030ecf3bf0 Mon Sep 17 00:00:00 2001 From: Aaron Wagener Date: Sun, 22 Oct 2023 06:00:27 -0500 Subject: [PATCH 28/54] Tests: modern PEP8-ify core test modules and methods (#2298) * rename modules * rename methods * add docstrings to the general tests * add base import stub * test_base -> bases * print deprecation warning * redo 2346 --- docs/world api.md | 3 +- test/TestBase.py | 313 +----------------- test/bases.py | 309 +++++++++++++++++ test/general/__init__.py | 7 + test/general/{TestFill.py => test_fill.py} | 48 ++- .../{TestHelpers.py => test_helpers.py} | 13 +- .../{TestHostYAML.py => test_host_yaml.py} | 2 + test/general/{TestIDs.py => test_ids.py} | 20 +- ...TestImplemented.py => test_implemented.py} | 6 +- test/general/{TestItems.py => test_items.py} | 10 +- .../{TestLocations.py => test_locations.py} | 8 +- test/general/{TestNames.py => test_names.py} | 4 +- .../{TestOptions.py => test_options.py} | 3 +- ...stReachability.py => test_reachability.py} | 6 +- ...ocationStore.py => test_location_store.py} | 0 .../data/{OnePlayer => one_player}/test.yaml | 0 .../{TestGenerate.py => test_generate.py} | 2 +- ...estMultiServer.py => test_multi_server.py} | 0 .../{TestSIPrefix.py => test_si_prefix.py} | 0 ...estAPIGenerate.py => test_api_generate.py} | 4 +- test/webhost/{TestDocs.py => test_docs.py} | 4 +- ...eGeneration.py => test_file_generation.py} | 4 +- 22 files changed, 410 insertions(+), 356 deletions(-) create mode 100644 test/bases.py rename test/general/{TestFill.py => test_fill.py} (92%) rename test/general/{TestHelpers.py => test_helpers.py} (90%) rename test/general/{TestHostYAML.py => test_host_yaml.py} (87%) rename test/general/{TestIDs.py => test_ids.py} (82%) rename test/general/{TestImplemented.py => test_implemented.py} (93%) rename test/general/{TestItems.py => test_items.py} (88%) rename test/general/{TestLocations.py => test_locations.py} (96%) rename test/general/{TestNames.py => test_names.py} (92%) rename test/general/{TestOptions.py => test_options.py} (78%) rename test/general/{TestReachability.py => test_reachability.py} (91%) rename test/netutils/{TestLocationStore.py => test_location_store.py} (100%) rename test/programs/data/{OnePlayer => one_player}/test.yaml (100%) rename test/programs/{TestGenerate.py => test_generate.py} (98%) rename test/programs/{TestMultiServer.py => test_multi_server.py} (100%) rename test/utils/{TestSIPrefix.py => test_si_prefix.py} (100%) rename test/webhost/{TestAPIGenerate.py => test_api_generate.py} (93%) rename test/webhost/{TestDocs.py => test_docs.py} (96%) rename test/webhost/{TestFileGeneration.py => test_file_generation.py} (96%) diff --git a/docs/world api.md b/docs/world api.md index 6fb5b3ac9c..b128e2b146 100644 --- a/docs/world api.md +++ b/docs/world api.md @@ -759,8 +759,9 @@ multiworld for each test written using it. Within subsequent modules, classes sh TestBase, and can then define options to test in the class body, and run tests in each test method. Example `__init__.py` + ```python -from test.TestBase import WorldTestBase +from test.test_base import WorldTestBase class MyGameTestBase(WorldTestBase): diff --git a/test/TestBase.py b/test/TestBase.py index ca7a19815c..bfd92346d3 100644 --- a/test/TestBase.py +++ b/test/TestBase.py @@ -1,310 +1,3 @@ -import typing -import unittest -from argparse import Namespace - -from test.general import gen_steps -from worlds import AutoWorld -from worlds.AutoWorld import call_all - -from BaseClasses import Location, MultiWorld, CollectionState, ItemClassification, Item -from worlds.alttp.Items import ItemFactory - - -class TestBase(unittest.TestCase): - multiworld: MultiWorld - _state_cache = {} - - def get_state(self, items): - if (self.multiworld, tuple(items)) in self._state_cache: - return self._state_cache[self.multiworld, tuple(items)] - state = CollectionState(self.multiworld) - for item in items: - item.classification = ItemClassification.progression - state.collect(item, event=True) - state.sweep_for_events() - state.update_reachable_regions(1) - self._state_cache[self.multiworld, tuple(items)] = state - return state - - def get_path(self, state, region): - def flist_to_iter(node): - while node: - value, node = node - yield value - - from itertools import zip_longest - reversed_path_as_flist = state.path.get(region, (region, None)) - string_path_flat = reversed(list(map(str, flist_to_iter(reversed_path_as_flist)))) - # Now we combine the flat string list into (region, exit) pairs - pathsiter = iter(string_path_flat) - pathpairs = zip_longest(pathsiter, pathsiter) - return list(pathpairs) - - def run_location_tests(self, access_pool): - for i, (location, access, *item_pool) in enumerate(access_pool): - items = item_pool[0] - all_except = item_pool[1] if len(item_pool) > 1 else None - state = self._get_items(item_pool, all_except) - path = self.get_path(state, self.multiworld.get_location(location, 1).parent_region) - with self.subTest(msg="Reach Location", location=location, access=access, items=items, - all_except=all_except, path=path, entry=i): - - self.assertEqual(self.multiworld.get_location(location, 1).can_reach(state), access, - f"failed {self.multiworld.get_location(location, 1)} with: {item_pool}") - - # check for partial solution - if not all_except and access: # we are not supposed to be able to reach location with partial inventory - for missing_item in item_pool[0]: - with self.subTest(msg="Location reachable without required item", location=location, - items=item_pool[0], missing_item=missing_item, entry=i): - state = self._get_items_partial(item_pool, missing_item) - - self.assertEqual(self.multiworld.get_location(location, 1).can_reach(state), False, - f"failed {self.multiworld.get_location(location, 1)}: succeeded with " - f"{missing_item} removed from: {item_pool}") - - def run_entrance_tests(self, access_pool): - for i, (entrance, access, *item_pool) in enumerate(access_pool): - items = item_pool[0] - all_except = item_pool[1] if len(item_pool) > 1 else None - state = self._get_items(item_pool, all_except) - path = self.get_path(state, self.multiworld.get_entrance(entrance, 1).parent_region) - with self.subTest(msg="Reach Entrance", entrance=entrance, access=access, items=items, - all_except=all_except, path=path, entry=i): - - self.assertEqual(self.multiworld.get_entrance(entrance, 1).can_reach(state), access) - - # check for partial solution - if not all_except and access: # we are not supposed to be able to reach location with partial inventory - for missing_item in item_pool[0]: - with self.subTest(msg="Entrance reachable without required item", entrance=entrance, - items=item_pool[0], missing_item=missing_item, entry=i): - state = self._get_items_partial(item_pool, missing_item) - self.assertEqual(self.multiworld.get_entrance(entrance, 1).can_reach(state), False, - f"failed {self.multiworld.get_entrance(entrance, 1)} with: {item_pool}") - - def _get_items(self, item_pool, all_except): - if all_except and len(all_except) > 0: - items = self.multiworld.itempool[:] - items = [item for item in items if - item.name not in all_except and not ("Bottle" in item.name and "AnyBottle" in all_except)] - items.extend(ItemFactory(item_pool[0], 1)) - else: - items = ItemFactory(item_pool[0], 1) - return self.get_state(items) - - def _get_items_partial(self, item_pool, missing_item): - new_items = item_pool[0].copy() - new_items.remove(missing_item) - items = ItemFactory(new_items, 1) - return self.get_state(items) - - -class WorldTestBase(unittest.TestCase): - options: typing.Dict[str, typing.Any] = {} - multiworld: MultiWorld - - game: typing.ClassVar[str] # define game name in subclass, example "Secret of Evermore" - auto_construct: typing.ClassVar[bool] = True - """ automatically set up a world for each test in this class """ - - def setUp(self) -> None: - if self.auto_construct: - self.world_setup() - - def world_setup(self, seed: typing.Optional[int] = None) -> None: - if type(self) is WorldTestBase or \ - (hasattr(WorldTestBase, self._testMethodName) - and not self.run_default_tests and - getattr(self, self._testMethodName).__code__ is - getattr(WorldTestBase, self._testMethodName, None).__code__): - return # setUp gets called for tests defined in the base class. We skip world_setup here. - if not hasattr(self, "game"): - raise NotImplementedError("didn't define game name") - self.multiworld = MultiWorld(1) - self.multiworld.game[1] = self.game - self.multiworld.player_name = {1: "Tester"} - self.multiworld.set_seed(seed) - self.multiworld.state = CollectionState(self.multiworld) - args = Namespace() - for name, option in AutoWorld.AutoWorldRegister.world_types[self.game].options_dataclass.type_hints.items(): - setattr(args, name, { - 1: option.from_any(self.options.get(name, getattr(option, "default"))) - }) - self.multiworld.set_options(args) - for step in gen_steps: - call_all(self.multiworld, step) - - # methods that can be called within tests - def collect_all_but(self, item_names: typing.Union[str, typing.Iterable[str]], - state: typing.Optional[CollectionState] = None) -> None: - """Collects all pre-placed items and items in the multiworld itempool except those provided""" - if isinstance(item_names, str): - item_names = (item_names,) - if not state: - state = self.multiworld.state - for item in self.multiworld.get_items(): - if item.name not in item_names: - state.collect(item) - - def get_item_by_name(self, item_name: str) -> Item: - """Returns the first item found in placed items, or in the itempool with the matching name""" - for item in self.multiworld.get_items(): - if item.name == item_name: - return item - raise ValueError("No such item") - - def get_items_by_name(self, item_names: typing.Union[str, typing.Iterable[str]]) -> typing.List[Item]: - """Returns actual items from the itempool that match the provided name(s)""" - if isinstance(item_names, str): - item_names = (item_names,) - return [item for item in self.multiworld.itempool if item.name in item_names] - - def collect_by_name(self, item_names: typing.Union[str, typing.Iterable[str]]) -> typing.List[Item]: - """ collect all of the items in the item pool that have the given names """ - items = self.get_items_by_name(item_names) - self.collect(items) - return items - - def collect(self, items: typing.Union[Item, typing.Iterable[Item]]) -> None: - """Collects the provided item(s) into state""" - if isinstance(items, Item): - items = (items,) - for item in items: - self.multiworld.state.collect(item) - - def remove_by_name(self, item_names: typing.Union[str, typing.Iterable[str]]) -> typing.List[Item]: - """Remove all of the items in the item pool with the given names from state""" - items = self.get_items_by_name(item_names) - self.remove(items) - return items - - def remove(self, items: typing.Union[Item, typing.Iterable[Item]]) -> None: - """Removes the provided item(s) from state""" - if isinstance(items, Item): - items = (items,) - for item in items: - if item.location and item.location.event and item.location in self.multiworld.state.events: - self.multiworld.state.events.remove(item.location) - self.multiworld.state.remove(item) - - def can_reach_location(self, location: str) -> bool: - """Determines if the current state can reach the provided location name""" - return self.multiworld.state.can_reach(location, "Location", 1) - - def can_reach_entrance(self, entrance: str) -> bool: - """Determines if the current state can reach the provided entrance name""" - return self.multiworld.state.can_reach(entrance, "Entrance", 1) - - def can_reach_region(self, region: str) -> bool: - """Determines if the current state can reach the provided region name""" - return self.multiworld.state.can_reach(region, "Region", 1) - - def count(self, item_name: str) -> int: - """Returns the amount of an item currently in state""" - return self.multiworld.state.count(item_name, 1) - - def assertAccessDependency(self, - locations: typing.List[str], - possible_items: typing.Iterable[typing.Iterable[str]], - only_check_listed: bool = False) -> None: - """Asserts that the provided locations can't be reached without the listed items but can be reached with any - one of the provided combinations""" - all_items = [item_name for item_names in possible_items for item_name in item_names] - - state = CollectionState(self.multiworld) - self.collect_all_but(all_items, state) - if only_check_listed: - for location in locations: - self.assertFalse(state.can_reach(location, "Location", 1), f"{location} is reachable without {all_items}") - else: - for location in self.multiworld.get_locations(): - loc_reachable = state.can_reach(location, "Location", 1) - self.assertEqual(loc_reachable, location.name not in locations, - f"{location.name} is reachable without {all_items}" if loc_reachable - else f"{location.name} is not reachable without {all_items}") - for item_names in possible_items: - items = self.get_items_by_name(item_names) - for item in items: - state.collect(item) - for location in locations: - self.assertTrue(state.can_reach(location, "Location", 1), - f"{location} not reachable with {item_names}") - for item in items: - state.remove(item) - - def assertBeatable(self, beatable: bool): - """Asserts that the game can be beaten with the current state""" - self.assertEqual(self.multiworld.can_beat_game(self.multiworld.state), beatable) - - # following tests are automatically run - @property - def run_default_tests(self) -> bool: - """Not possible or identical to the base test that's always being run already""" - return (self.options - or self.setUp.__code__ is not WorldTestBase.setUp.__code__ - or self.world_setup.__code__ is not WorldTestBase.world_setup.__code__) - - @property - def constructed(self) -> bool: - """A multiworld has been constructed by this point""" - return hasattr(self, "game") and hasattr(self, "multiworld") - - def testAllStateCanReachEverything(self): - """Ensure all state can reach everything and complete the game with the defined options""" - if not (self.run_default_tests and self.constructed): - return - with self.subTest("Game", game=self.game): - excluded = self.multiworld.exclude_locations[1].value - state = self.multiworld.get_all_state(False) - for location in self.multiworld.get_locations(): - if location.name not in excluded: - with self.subTest("Location should be reached", location=location): - reachable = location.can_reach(state) - self.assertTrue(reachable, f"{location.name} unreachable") - with self.subTest("Beatable"): - self.multiworld.state = state - self.assertBeatable(True) - - def testEmptyStateCanReachSomething(self): - """Ensure empty state can reach at least one location with the defined options""" - if not (self.run_default_tests and self.constructed): - return - with self.subTest("Game", game=self.game): - state = CollectionState(self.multiworld) - locations = self.multiworld.get_reachable_locations(state, 1) - self.assertGreater(len(locations), 0, - "Need to be able to reach at least one location to get started.") - - def testFill(self): - """Generates a multiworld and validates placements with the defined options""" - if not (self.run_default_tests and self.constructed): - return - from Fill import distribute_items_restrictive - - # basically a shortened reimplementation of this method from core, in order to force the check is done - def fulfills_accessibility(): - locations = self.multiworld.get_locations(1).copy() - state = CollectionState(self.multiworld) - while locations: - sphere: typing.List[Location] = [] - for n in range(len(locations) - 1, -1, -1): - if locations[n].can_reach(state): - sphere.append(locations.pop(n)) - self.assertTrue(sphere or self.multiworld.accessibility[1] == "minimal", - f"Unreachable locations: {locations}") - if not sphere: - break - for location in sphere: - if location.item: - state.collect(location.item, True, location) - - return self.multiworld.has_beaten_game(state, 1) - - with self.subTest("Game", game=self.game, seed=self.multiworld.seed): - distribute_items_restrictive(self.multiworld) - call_all(self.multiworld, "post_fill") - self.assertTrue(fulfills_accessibility(), "Collected all locations, but can't beat the game.") - placed_items = [loc.item for loc in self.multiworld.get_locations() if loc.item and loc.item.code] - self.assertLessEqual(len(self.multiworld.itempool), len(placed_items), - "Unplaced Items remaining in itempool") +from .bases import TestBase, WorldTestBase +from warnings import warn +warn("TestBase was renamed to bases", DeprecationWarning) diff --git a/test/bases.py b/test/bases.py new file mode 100644 index 0000000000..5fe4df2014 --- /dev/null +++ b/test/bases.py @@ -0,0 +1,309 @@ +import typing +import unittest +from argparse import Namespace + +from test.general import gen_steps +from worlds import AutoWorld +from worlds.AutoWorld import call_all + +from BaseClasses import Location, MultiWorld, CollectionState, ItemClassification, Item +from worlds.alttp.Items import ItemFactory + + +class TestBase(unittest.TestCase): + multiworld: MultiWorld + _state_cache = {} + + def get_state(self, items): + if (self.multiworld, tuple(items)) in self._state_cache: + return self._state_cache[self.multiworld, tuple(items)] + state = CollectionState(self.multiworld) + for item in items: + item.classification = ItemClassification.progression + state.collect(item, event=True) + state.sweep_for_events() + state.update_reachable_regions(1) + self._state_cache[self.multiworld, tuple(items)] = state + return state + + def get_path(self, state, region): + def flist_to_iter(node): + while node: + value, node = node + yield value + + from itertools import zip_longest + reversed_path_as_flist = state.path.get(region, (region, None)) + string_path_flat = reversed(list(map(str, flist_to_iter(reversed_path_as_flist)))) + # Now we combine the flat string list into (region, exit) pairs + pathsiter = iter(string_path_flat) + pathpairs = zip_longest(pathsiter, pathsiter) + return list(pathpairs) + + def run_location_tests(self, access_pool): + for i, (location, access, *item_pool) in enumerate(access_pool): + items = item_pool[0] + all_except = item_pool[1] if len(item_pool) > 1 else None + state = self._get_items(item_pool, all_except) + path = self.get_path(state, self.multiworld.get_location(location, 1).parent_region) + with self.subTest(msg="Reach Location", location=location, access=access, items=items, + all_except=all_except, path=path, entry=i): + + self.assertEqual(self.multiworld.get_location(location, 1).can_reach(state), access, + f"failed {self.multiworld.get_location(location, 1)} with: {item_pool}") + + # check for partial solution + if not all_except and access: # we are not supposed to be able to reach location with partial inventory + for missing_item in item_pool[0]: + with self.subTest(msg="Location reachable without required item", location=location, + items=item_pool[0], missing_item=missing_item, entry=i): + state = self._get_items_partial(item_pool, missing_item) + + self.assertEqual(self.multiworld.get_location(location, 1).can_reach(state), False, + f"failed {self.multiworld.get_location(location, 1)}: succeeded with " + f"{missing_item} removed from: {item_pool}") + + def run_entrance_tests(self, access_pool): + for i, (entrance, access, *item_pool) in enumerate(access_pool): + items = item_pool[0] + all_except = item_pool[1] if len(item_pool) > 1 else None + state = self._get_items(item_pool, all_except) + path = self.get_path(state, self.multiworld.get_entrance(entrance, 1).parent_region) + with self.subTest(msg="Reach Entrance", entrance=entrance, access=access, items=items, + all_except=all_except, path=path, entry=i): + + self.assertEqual(self.multiworld.get_entrance(entrance, 1).can_reach(state), access) + + # check for partial solution + if not all_except and access: # we are not supposed to be able to reach location with partial inventory + for missing_item in item_pool[0]: + with self.subTest(msg="Entrance reachable without required item", entrance=entrance, + items=item_pool[0], missing_item=missing_item, entry=i): + state = self._get_items_partial(item_pool, missing_item) + self.assertEqual(self.multiworld.get_entrance(entrance, 1).can_reach(state), False, + f"failed {self.multiworld.get_entrance(entrance, 1)} with: {item_pool}") + + def _get_items(self, item_pool, all_except): + if all_except and len(all_except) > 0: + items = self.multiworld.itempool[:] + items = [item for item in items if + item.name not in all_except and not ("Bottle" in item.name and "AnyBottle" in all_except)] + items.extend(ItemFactory(item_pool[0], 1)) + else: + items = ItemFactory(item_pool[0], 1) + return self.get_state(items) + + def _get_items_partial(self, item_pool, missing_item): + new_items = item_pool[0].copy() + new_items.remove(missing_item) + items = ItemFactory(new_items, 1) + return self.get_state(items) + + +class WorldTestBase(unittest.TestCase): + options: typing.Dict[str, typing.Any] = {} + multiworld: MultiWorld + + game: typing.ClassVar[str] # define game name in subclass, example "Secret of Evermore" + auto_construct: typing.ClassVar[bool] = True + """ automatically set up a world for each test in this class """ + + def setUp(self) -> None: + if self.auto_construct: + self.world_setup() + + def world_setup(self, seed: typing.Optional[int] = None) -> None: + if type(self) is WorldTestBase or \ + (hasattr(WorldTestBase, self._testMethodName) + and not self.run_default_tests and + getattr(self, self._testMethodName).__code__ is + getattr(WorldTestBase, self._testMethodName, None).__code__): + return # setUp gets called for tests defined in the base class. We skip world_setup here. + if not hasattr(self, "game"): + raise NotImplementedError("didn't define game name") + self.multiworld = MultiWorld(1) + self.multiworld.game[1] = self.game + self.multiworld.player_name = {1: "Tester"} + self.multiworld.set_seed(seed) + self.multiworld.state = CollectionState(self.multiworld) + args = Namespace() + for name, option in AutoWorld.AutoWorldRegister.world_types[self.game].options_dataclass.type_hints.items(): + setattr(args, name, { + 1: option.from_any(self.options.get(name, getattr(option, "default"))) + }) + self.multiworld.set_options(args) + for step in gen_steps: + call_all(self.multiworld, step) + + # methods that can be called within tests + def collect_all_but(self, item_names: typing.Union[str, typing.Iterable[str]], + state: typing.Optional[CollectionState] = None) -> None: + """Collects all pre-placed items and items in the multiworld itempool except those provided""" + if isinstance(item_names, str): + item_names = (item_names,) + if not state: + state = self.multiworld.state + for item in self.multiworld.get_items(): + if item.name not in item_names: + state.collect(item) + + def get_item_by_name(self, item_name: str) -> Item: + """Returns the first item found in placed items, or in the itempool with the matching name""" + for item in self.multiworld.get_items(): + if item.name == item_name: + return item + raise ValueError("No such item") + + def get_items_by_name(self, item_names: typing.Union[str, typing.Iterable[str]]) -> typing.List[Item]: + """Returns actual items from the itempool that match the provided name(s)""" + if isinstance(item_names, str): + item_names = (item_names,) + return [item for item in self.multiworld.itempool if item.name in item_names] + + def collect_by_name(self, item_names: typing.Union[str, typing.Iterable[str]]) -> typing.List[Item]: + """ collect all of the items in the item pool that have the given names """ + items = self.get_items_by_name(item_names) + self.collect(items) + return items + + def collect(self, items: typing.Union[Item, typing.Iterable[Item]]) -> None: + """Collects the provided item(s) into state""" + if isinstance(items, Item): + items = (items,) + for item in items: + self.multiworld.state.collect(item) + + def remove_by_name(self, item_names: typing.Union[str, typing.Iterable[str]]) -> typing.List[Item]: + """Remove all of the items in the item pool with the given names from state""" + items = self.get_items_by_name(item_names) + self.remove(items) + return items + + def remove(self, items: typing.Union[Item, typing.Iterable[Item]]) -> None: + """Removes the provided item(s) from state""" + if isinstance(items, Item): + items = (items,) + for item in items: + if item.location and item.location.event and item.location in self.multiworld.state.events: + self.multiworld.state.events.remove(item.location) + self.multiworld.state.remove(item) + + def can_reach_location(self, location: str) -> bool: + """Determines if the current state can reach the provided location name""" + return self.multiworld.state.can_reach(location, "Location", 1) + + def can_reach_entrance(self, entrance: str) -> bool: + """Determines if the current state can reach the provided entrance name""" + return self.multiworld.state.can_reach(entrance, "Entrance", 1) + + def can_reach_region(self, region: str) -> bool: + """Determines if the current state can reach the provided region name""" + return self.multiworld.state.can_reach(region, "Region", 1) + + def count(self, item_name: str) -> int: + """Returns the amount of an item currently in state""" + return self.multiworld.state.count(item_name, 1) + + def assertAccessDependency(self, + locations: typing.List[str], + possible_items: typing.Iterable[typing.Iterable[str]], + only_check_listed: bool = False) -> None: + """Asserts that the provided locations can't be reached without the listed items but can be reached with any + one of the provided combinations""" + all_items = [item_name for item_names in possible_items for item_name in item_names] + + state = CollectionState(self.multiworld) + self.collect_all_but(all_items, state) + if only_check_listed: + for location in locations: + self.assertFalse(state.can_reach(location, "Location", 1), f"{location} is reachable without {all_items}") + else: + for location in self.multiworld.get_locations(): + loc_reachable = state.can_reach(location, "Location", 1) + self.assertEqual(loc_reachable, location.name not in locations, + f"{location.name} is reachable without {all_items}" if loc_reachable + else f"{location.name} is not reachable without {all_items}") + for item_names in possible_items: + items = self.get_items_by_name(item_names) + for item in items: + state.collect(item) + for location in locations: + self.assertTrue(state.can_reach(location, "Location", 1), + f"{location} not reachable with {item_names}") + for item in items: + state.remove(item) + + def assertBeatable(self, beatable: bool): + """Asserts that the game can be beaten with the current state""" + self.assertEqual(self.multiworld.can_beat_game(self.multiworld.state), beatable) + + # following tests are automatically run + @property + def run_default_tests(self) -> bool: + """Not possible or identical to the base test that's always being run already""" + return (self.options + or self.setUp.__code__ is not WorldTestBase.setUp.__code__ + or self.world_setup.__code__ is not WorldTestBase.world_setup.__code__) + + @property + def constructed(self) -> bool: + """A multiworld has been constructed by this point""" + return hasattr(self, "game") and hasattr(self, "multiworld") + + def test_all_state_can_reach_everything(self): + """Ensure all state can reach everything and complete the game with the defined options""" + if not (self.run_default_tests and self.constructed): + return + with self.subTest("Game", game=self.game): + excluded = self.multiworld.exclude_locations[1].value + state = self.multiworld.get_all_state(False) + for location in self.multiworld.get_locations(): + if location.name not in excluded: + with self.subTest("Location should be reached", location=location): + reachable = location.can_reach(state) + self.assertTrue(reachable, f"{location.name} unreachable") + with self.subTest("Beatable"): + self.multiworld.state = state + self.assertBeatable(True) + + def test_empty_state_can_reach_something(self): + """Ensure empty state can reach at least one location with the defined options""" + if not (self.run_default_tests and self.constructed): + return + with self.subTest("Game", game=self.game): + state = CollectionState(self.multiworld) + locations = self.multiworld.get_reachable_locations(state, 1) + self.assertGreater(len(locations), 0, + "Need to be able to reach at least one location to get started.") + + def test_fill(self): + """Generates a multiworld and validates placements with the defined options""" + if not (self.run_default_tests and self.constructed): + return + from Fill import distribute_items_restrictive + + # basically a shortened reimplementation of this method from core, in order to force the check is done + def fulfills_accessibility() -> bool: + locations = self.multiworld.get_locations(1).copy() + state = CollectionState(self.multiworld) + while locations: + sphere: typing.List[Location] = [] + for n in range(len(locations) - 1, -1, -1): + if locations[n].can_reach(state): + sphere.append(locations.pop(n)) + self.assertTrue(sphere or self.multiworld.accessibility[1] == "minimal", + f"Unreachable locations: {locations}") + if not sphere: + break + for location in sphere: + if location.item: + state.collect(location.item, True, location) + return self.multiworld.has_beaten_game(state, 1) + + with self.subTest("Game", game=self.game, seed=self.multiworld.seed): + distribute_items_restrictive(self.multiworld) + call_all(self.multiworld, "post_fill") + self.assertTrue(fulfills_accessibility(), "Collected all locations, but can't beat the game.") + placed_items = [loc.item for loc in self.multiworld.get_locations() if loc.item and loc.item.code] + self.assertLessEqual(len(self.multiworld.itempool), len(placed_items), + "Unplaced Items remaining in itempool") diff --git a/test/general/__init__.py b/test/general/__init__.py index d7ecc95749..5e0f22f4ec 100644 --- a/test/general/__init__.py +++ b/test/general/__init__.py @@ -8,6 +8,13 @@ gen_steps = ("generate_early", "create_regions", "create_items", "set_rules", "g def setup_solo_multiworld(world_type: Type[World], steps: Tuple[str, ...] = gen_steps) -> MultiWorld: + """ + Creates a multiworld with a single player of `world_type`, sets default options, and calls provided gen steps. + + :param world_type: Type of the world to generate a multiworld for + :param steps: The gen steps that should be called on the generated multiworld before returning. Default calls + steps through pre_fill + """ multiworld = MultiWorld(1) multiworld.game[1] = world_type.game multiworld.player_name = {1: "Tester"} diff --git a/test/general/TestFill.py b/test/general/test_fill.py similarity index 92% rename from test/general/TestFill.py rename to test/general/test_fill.py index 0933603dfd..4e8cc2edb7 100644 --- a/test/general/TestFill.py +++ b/test/general/test_fill.py @@ -72,7 +72,7 @@ class PlayerDefinition(object): return region -def fillRegion(world: MultiWorld, region: Region, items: List[Item]) -> List[Item]: +def fill_region(world: MultiWorld, region: Region, items: List[Item]) -> List[Item]: items = items.copy() while len(items) > 0: location = region.locations.pop(0) @@ -86,7 +86,7 @@ def fillRegion(world: MultiWorld, region: Region, items: List[Item]) -> List[Ite return items -def regionContains(region: Region, item: Item) -> bool: +def region_contains(region: Region, item: Item) -> bool: for location in region.locations: if location.item == item: return True @@ -133,6 +133,7 @@ def names(objs: list) -> Iterable[str]: class TestFillRestrictive(unittest.TestCase): def test_basic_fill(self): + """Tests `fill_restrictive` fills and removes the locations and items from their respective lists""" multi_world = generate_multi_world() player1 = generate_player_data(multi_world, 1, 2, 2) @@ -150,6 +151,7 @@ class TestFillRestrictive(unittest.TestCase): self.assertEqual([], player1.prog_items) def test_ordered_fill(self): + """Tests `fill_restrictive` fulfills set rules""" multi_world = generate_multi_world() player1 = generate_player_data(multi_world, 1, 2, 2) items = player1.prog_items @@ -166,6 +168,7 @@ class TestFillRestrictive(unittest.TestCase): self.assertEqual(locations[1].item, items[1]) def test_partial_fill(self): + """Tests that `fill_restrictive` returns unfilled locations""" multi_world = generate_multi_world() player1 = generate_player_data(multi_world, 1, 3, 2) @@ -191,6 +194,7 @@ class TestFillRestrictive(unittest.TestCase): self.assertEqual(player1.locations[0], loc2) def test_minimal_fill(self): + """Test that fill for minimal player can have unreachable items""" multi_world = generate_multi_world() player1 = generate_player_data(multi_world, 1, 2, 2) @@ -246,6 +250,7 @@ class TestFillRestrictive(unittest.TestCase): f'{item} is unreachable in {item.location}') def test_reversed_fill(self): + """Test a different set of rules can be satisfied""" multi_world = generate_multi_world() player1 = generate_player_data(multi_world, 1, 2, 2) @@ -264,6 +269,7 @@ class TestFillRestrictive(unittest.TestCase): self.assertEqual(loc1.item, item0) def test_multi_step_fill(self): + """Test that fill is able to satisfy multiple spheres""" multi_world = generate_multi_world() player1 = generate_player_data(multi_world, 1, 4, 4) @@ -288,6 +294,7 @@ class TestFillRestrictive(unittest.TestCase): self.assertEqual(locations[3].item, items[3]) def test_impossible_fill(self): + """Test that fill raises an error when it can't place any items""" multi_world = generate_multi_world() player1 = generate_player_data(multi_world, 1, 2, 2) items = player1.prog_items @@ -304,6 +311,7 @@ class TestFillRestrictive(unittest.TestCase): player1.locations.copy(), player1.prog_items.copy()) def test_circular_fill(self): + """Test that fill raises an error when it can't place all items""" multi_world = generate_multi_world() player1 = generate_player_data(multi_world, 1, 3, 3) @@ -324,6 +332,7 @@ class TestFillRestrictive(unittest.TestCase): player1.locations.copy(), player1.prog_items.copy()) def test_competing_fill(self): + """Test that fill raises an error when it can't place items in a way to satisfy the conditions""" multi_world = generate_multi_world() player1 = generate_player_data(multi_world, 1, 2, 2) @@ -340,6 +349,7 @@ class TestFillRestrictive(unittest.TestCase): player1.locations.copy(), player1.prog_items.copy()) def test_multiplayer_fill(self): + """Test that items can be placed across worlds""" multi_world = generate_multi_world(2) player1 = generate_player_data(multi_world, 1, 2, 2) player2 = generate_player_data(multi_world, 2, 2, 2) @@ -360,6 +370,7 @@ class TestFillRestrictive(unittest.TestCase): self.assertEqual(player2.locations[1].item, player2.prog_items[0]) def test_multiplayer_rules_fill(self): + """Test that fill across worlds satisfies the rules""" multi_world = generate_multi_world(2) player1 = generate_player_data(multi_world, 1, 2, 2) player2 = generate_player_data(multi_world, 2, 2, 2) @@ -383,6 +394,7 @@ class TestFillRestrictive(unittest.TestCase): self.assertEqual(player2.locations[1].item, player1.prog_items[1]) def test_restrictive_progress(self): + """Test that various spheres with different requirements can be filled""" multi_world = generate_multi_world() player1 = generate_player_data(multi_world, 1, prog_item_count=25) items = player1.prog_items.copy() @@ -405,6 +417,7 @@ class TestFillRestrictive(unittest.TestCase): locations, player1.prog_items) def test_swap_to_earlier_location_with_item_rule(self): + """Test that item swap happens and works as intended""" # test for PR#1109 multi_world = generate_multi_world(1) player1 = generate_player_data(multi_world, 1, 4, 4) @@ -430,6 +443,7 @@ class TestFillRestrictive(unittest.TestCase): self.assertEqual(sphere1_loc.item, allowed_item, "Wrong item in Sphere 1") def test_double_sweep(self): + """Test that sweep doesn't duplicate Event items when sweeping""" # test for PR1114 multi_world = generate_multi_world(1) player1 = generate_player_data(multi_world, 1, 1, 1) @@ -445,6 +459,7 @@ class TestFillRestrictive(unittest.TestCase): self.assertEqual(multi_world.state.prog_items[item.name, item.player], 1, "Sweep collected multiple times") def test_correct_item_instance_removed_from_pool(self): + """Test that a placed item gets removed from the submitted pool""" multi_world = generate_multi_world() player1 = generate_player_data(multi_world, 1, 2, 2) @@ -461,6 +476,7 @@ class TestFillRestrictive(unittest.TestCase): class TestDistributeItemsRestrictive(unittest.TestCase): def test_basic_distribute(self): + """Test that distribute_items_restrictive is deterministic""" multi_world = generate_multi_world() player1 = generate_player_data( multi_world, 1, 4, prog_item_count=2, basic_item_count=2) @@ -480,6 +496,7 @@ class TestDistributeItemsRestrictive(unittest.TestCase): self.assertFalse(locations[3].event) def test_excluded_distribute(self): + """Test that distribute_items_restrictive doesn't put advancement items on excluded locations""" multi_world = generate_multi_world() player1 = generate_player_data( multi_world, 1, 4, prog_item_count=2, basic_item_count=2) @@ -494,6 +511,7 @@ class TestDistributeItemsRestrictive(unittest.TestCase): self.assertFalse(locations[2].item.advancement) def test_non_excluded_item_distribute(self): + """Test that useful items aren't placed on excluded locations""" multi_world = generate_multi_world() player1 = generate_player_data( multi_world, 1, 4, prog_item_count=2, basic_item_count=2) @@ -508,6 +526,7 @@ class TestDistributeItemsRestrictive(unittest.TestCase): self.assertEqual(locations[1].item, basic_items[0]) def test_too_many_excluded_distribute(self): + """Test that fill fails if it can't place all progression items due to too many excluded locations""" multi_world = generate_multi_world() player1 = generate_player_data( multi_world, 1, 4, prog_item_count=2, basic_item_count=2) @@ -520,6 +539,7 @@ class TestDistributeItemsRestrictive(unittest.TestCase): self.assertRaises(FillError, distribute_items_restrictive, multi_world) def test_non_excluded_item_must_distribute(self): + """Test that fill fails if it can't place useful items due to too many excluded locations""" multi_world = generate_multi_world() player1 = generate_player_data( multi_world, 1, 4, prog_item_count=2, basic_item_count=2) @@ -534,6 +554,7 @@ class TestDistributeItemsRestrictive(unittest.TestCase): self.assertRaises(FillError, distribute_items_restrictive, multi_world) def test_priority_distribute(self): + """Test that priority locations receive advancement items""" multi_world = generate_multi_world() player1 = generate_player_data( multi_world, 1, 4, prog_item_count=2, basic_item_count=2) @@ -548,6 +569,7 @@ class TestDistributeItemsRestrictive(unittest.TestCase): self.assertTrue(locations[3].item.advancement) def test_excess_priority_distribute(self): + """Test that if there's more priority locations than advancement items, they can still fill""" multi_world = generate_multi_world() player1 = generate_player_data( multi_world, 1, 4, prog_item_count=2, basic_item_count=2) @@ -562,6 +584,7 @@ class TestDistributeItemsRestrictive(unittest.TestCase): self.assertFalse(locations[3].item.advancement) def test_multiple_world_priority_distribute(self): + """Test that priority fill can be satisfied for multiple worlds""" multi_world = generate_multi_world(3) player1 = generate_player_data( multi_world, 1, 4, prog_item_count=2, basic_item_count=2) @@ -591,7 +614,7 @@ class TestDistributeItemsRestrictive(unittest.TestCase): self.assertTrue(player3.locations[3].item.advancement) def test_can_remove_locations_in_fill_hook(self): - + """Test that distribute_items_restrictive calls the fill hook and allows for item and location removal""" multi_world = generate_multi_world() player1 = generate_player_data( multi_world, 1, 4, prog_item_count=2, basic_item_count=2) @@ -611,6 +634,7 @@ class TestDistributeItemsRestrictive(unittest.TestCase): self.assertIsNone(removed_location[0].item) def test_seed_robust_to_item_order(self): + """Test deterministic fill""" mw1 = generate_multi_world() gen1 = generate_player_data( mw1, 1, 4, prog_item_count=2, basic_item_count=2) @@ -628,6 +652,7 @@ class TestDistributeItemsRestrictive(unittest.TestCase): self.assertEqual(gen1.locations[3].item, gen2.locations[3].item) def test_seed_robust_to_location_order(self): + """Test deterministic fill even if locations in a region are reordered""" mw1 = generate_multi_world() gen1 = generate_player_data( mw1, 1, 4, prog_item_count=2, basic_item_count=2) @@ -646,6 +671,7 @@ class TestDistributeItemsRestrictive(unittest.TestCase): self.assertEqual(gen1.locations[3].item, gen2.locations[3].item) def test_can_reserve_advancement_items_for_general_fill(self): + """Test that priority locations fill still satisfies item rules""" multi_world = generate_multi_world() player1 = generate_player_data( multi_world, 1, location_count=5, prog_item_count=5) @@ -655,14 +681,14 @@ class TestDistributeItemsRestrictive(unittest.TestCase): location = player1.locations[0] location.progress_type = LocationProgressType.PRIORITY - location.item_rule = lambda item: item != items[ - 0] and item != items[1] and item != items[2] and item != items[3] + location.item_rule = lambda item: item not in items[:4] distribute_items_restrictive(multi_world) self.assertEqual(location.item, items[4]) def test_non_excluded_local_items(self): + """Test that local items get placed locally in a multiworld""" multi_world = generate_multi_world(2) player1 = generate_player_data( multi_world, 1, location_count=5, basic_item_count=5) @@ -683,6 +709,7 @@ class TestDistributeItemsRestrictive(unittest.TestCase): self.assertFalse(item.location.event, False) def test_early_items(self) -> None: + """Test that the early items API successfully places items early""" mw = generate_multi_world(2) player1 = generate_player_data(mw, 1, location_count=5, basic_item_count=5) player2 = generate_player_data(mw, 2, location_count=5, basic_item_count=5) @@ -762,21 +789,22 @@ class TestBalanceMultiworldProgression(unittest.TestCase): # Sphere 1 region = player1.generate_region(player1.menu, 20) - items = fillRegion(multi_world, region, [ + items = fill_region(multi_world, region, [ player1.prog_items[0]] + items) # Sphere 2 region = player1.generate_region( player1.regions[1], 20, lambda state: state.has(player1.prog_items[0].name, player1.id)) - items = fillRegion( + items = fill_region( multi_world, region, [player1.prog_items[1], player2.prog_items[0]] + items) # Sphere 3 region = player2.generate_region( player2.menu, 20, lambda state: state.has(player2.prog_items[0].name, player2.id)) - fillRegion(multi_world, region, [player2.prog_items[1]] + items) + fill_region(multi_world, region, [player2.prog_items[1]] + items) def test_balances_progression(self) -> None: + """Tests that progression balancing moves progression items earlier""" self.multi_world.progression_balancing[self.player1.id].value = 50 self.multi_world.progression_balancing[self.player2.id].value = 50 @@ -789,6 +817,7 @@ class TestBalanceMultiworldProgression(unittest.TestCase): self.player1.regions[1], self.player2.prog_items[0]) def test_balances_progression_light(self) -> None: + """Test that progression balancing still moves items earlier on minimum value""" self.multi_world.progression_balancing[self.player1.id].value = 1 self.multi_world.progression_balancing[self.player2.id].value = 1 @@ -802,6 +831,7 @@ class TestBalanceMultiworldProgression(unittest.TestCase): self.player1.regions[1], self.player2.prog_items[0]) def test_balances_progression_heavy(self) -> None: + """Test that progression balancing moves items earlier on maximum value""" self.multi_world.progression_balancing[self.player1.id].value = 99 self.multi_world.progression_balancing[self.player2.id].value = 99 @@ -815,6 +845,7 @@ class TestBalanceMultiworldProgression(unittest.TestCase): self.player1.regions[1], self.player2.prog_items[0]) def test_skips_balancing_progression(self) -> None: + """Test that progression balancing is skipped when players have it disabled""" self.multi_world.progression_balancing[self.player1.id].value = 0 self.multi_world.progression_balancing[self.player2.id].value = 0 @@ -827,6 +858,7 @@ class TestBalanceMultiworldProgression(unittest.TestCase): self.player1.regions[2], self.player2.prog_items[0]) def test_ignores_priority_locations(self) -> None: + """Test that progression items on priority locations don't get moved by balancing""" self.multi_world.progression_balancing[self.player1.id].value = 50 self.multi_world.progression_balancing[self.player2.id].value = 50 diff --git a/test/general/TestHelpers.py b/test/general/test_helpers.py similarity index 90% rename from test/general/TestHelpers.py rename to test/general/test_helpers.py index 17fdce653c..83b56b3438 100644 --- a/test/general/TestHelpers.py +++ b/test/general/test_helpers.py @@ -1,8 +1,7 @@ -from argparse import Namespace -from typing import Dict, Optional, Callable - -from BaseClasses import MultiWorld, CollectionState, Region import unittest +from typing import Callable, Dict, Optional + +from BaseClasses import CollectionState, MultiWorld, Region class TestHelpers(unittest.TestCase): @@ -15,7 +14,8 @@ class TestHelpers(unittest.TestCase): self.multiworld.player_name = {1: "Tester"} self.multiworld.set_seed() - def testRegionHelpers(self) -> None: + def test_region_helpers(self) -> None: + """Tests `Region.add_locations()` and `Region.add_exits()` have correct behavior""" regions: Dict[str, str] = { "TestRegion1": "I'm an apple", "TestRegion2": "I'm a banana", @@ -79,4 +79,5 @@ class TestHelpers(unittest.TestCase): current_region.add_exits(reg_exit_set[region]) exit_names = {_exit.name for _exit in current_region.exits} for reg_exit in reg_exit_set[region]: - self.assertTrue(f"{region} -> {reg_exit}" in exit_names, f"{region} -> {reg_exit} not in {exit_names}") + self.assertTrue(f"{region} -> {reg_exit}" in exit_names, + f"{region} -> {reg_exit} not in {exit_names}") diff --git a/test/general/TestHostYAML.py b/test/general/test_host_yaml.py similarity index 87% rename from test/general/TestHostYAML.py rename to test/general/test_host_yaml.py index f5fd406cac..9408f95b16 100644 --- a/test/general/TestHostYAML.py +++ b/test/general/test_host_yaml.py @@ -15,6 +15,7 @@ class TestIDs(unittest.TestCase): cls.yaml_options = Utils.parse_yaml(f.read()) def test_utils_in_yaml(self) -> None: + """Tests that the auto generated host.yaml has default settings in it""" for option_key, option_set in Utils.get_default_options().items(): with self.subTest(option_key): self.assertIn(option_key, self.yaml_options) @@ -22,6 +23,7 @@ class TestIDs(unittest.TestCase): self.assertIn(sub_option_key, self.yaml_options[option_key]) def test_yaml_in_utils(self) -> None: + """Tests that the auto generated host.yaml shows up in reference calls""" utils_options = Utils.get_default_options() for option_key, option_set in self.yaml_options.items(): with self.subTest(option_key): diff --git a/test/general/TestIDs.py b/test/general/test_ids.py similarity index 82% rename from test/general/TestIDs.py rename to test/general/test_ids.py index db1c9461b9..4edfb8d994 100644 --- a/test/general/TestIDs.py +++ b/test/general/test_ids.py @@ -3,35 +3,37 @@ from worlds.AutoWorld import AutoWorldRegister class TestIDs(unittest.TestCase): - def testUniqueItems(self): + def test_unique_items(self): + """Tests that every game has a unique ID per item in the datapackage""" known_item_ids = set() for gamename, world_type in AutoWorldRegister.world_types.items(): current = len(known_item_ids) known_item_ids |= set(world_type.item_id_to_name) self.assertEqual(len(known_item_ids) - len(world_type.item_id_to_name), current) - def testUniqueLocations(self): + def test_unique_locations(self): + """Tests that every game has a unique ID per location in the datapackage""" known_location_ids = set() for gamename, world_type in AutoWorldRegister.world_types.items(): current = len(known_location_ids) known_location_ids |= set(world_type.location_id_to_name) self.assertEqual(len(known_location_ids) - len(world_type.location_id_to_name), current) - def testRangeItems(self): + def test_range_items(self): """There are Javascript clients, which are limited to Number.MAX_SAFE_INTEGER due to 64bit float precision.""" for gamename, world_type in AutoWorldRegister.world_types.items(): with self.subTest(game=gamename): for item_id in world_type.item_id_to_name: self.assertLess(item_id, 2**53) - def testRangeLocations(self): + def test_range_locations(self): """There are Javascript clients, which are limited to Number.MAX_SAFE_INTEGER due to 64bit float precision.""" for gamename, world_type in AutoWorldRegister.world_types.items(): with self.subTest(game=gamename): for location_id in world_type.location_id_to_name: self.assertLess(location_id, 2**53) - def testReservedItems(self): + def test_reserved_items(self): """negative item IDs are reserved to the special "Archipelago" world.""" for gamename, world_type in AutoWorldRegister.world_types.items(): with self.subTest(game=gamename): @@ -42,7 +44,7 @@ class TestIDs(unittest.TestCase): for item_id in world_type.item_id_to_name: self.assertGreater(item_id, 0) - def testReservedLocations(self): + def test_reserved_locations(self): """negative location IDs are reserved to the special "Archipelago" world.""" for gamename, world_type in AutoWorldRegister.world_types.items(): with self.subTest(game=gamename): @@ -53,12 +55,14 @@ class TestIDs(unittest.TestCase): for location_id in world_type.location_id_to_name: self.assertGreater(location_id, 0) - def testDuplicateItemIDs(self): + def test_duplicate_item_ids(self): + """Test that a game doesn't have item id overlap within its own datapackage""" for gamename, world_type in AutoWorldRegister.world_types.items(): with self.subTest(game=gamename): self.assertEqual(len(world_type.item_id_to_name), len(world_type.item_name_to_id)) - def testDuplicateLocationIDs(self): + def test_duplicate_location_ids(self): + """Test that a game doesn't have location id overlap within its own datapackage""" for gamename, world_type in AutoWorldRegister.world_types.items(): with self.subTest(game=gamename): self.assertEqual(len(world_type.location_id_to_name), len(world_type.location_name_to_id)) diff --git a/test/general/TestImplemented.py b/test/general/test_implemented.py similarity index 93% rename from test/general/TestImplemented.py rename to test/general/test_implemented.py index 22c546eff1..67d0e5ff72 100644 --- a/test/general/TestImplemented.py +++ b/test/general/test_implemented.py @@ -5,7 +5,7 @@ from . import setup_solo_multiworld class TestImplemented(unittest.TestCase): - def testCompletionCondition(self): + def test_completion_condition(self): """Ensure a completion condition is set that has requirements.""" for game_name, world_type in AutoWorldRegister.world_types.items(): if not world_type.hidden and game_name not in {"Sudoku"}: @@ -13,7 +13,7 @@ class TestImplemented(unittest.TestCase): multiworld = setup_solo_multiworld(world_type) self.assertFalse(multiworld.completion_condition[1](multiworld.state)) - def testEntranceParents(self): + def test_entrance_parents(self): """Tests that the parents of created Entrances match the exiting Region.""" for game_name, world_type in AutoWorldRegister.world_types.items(): if not world_type.hidden: @@ -23,7 +23,7 @@ class TestImplemented(unittest.TestCase): for exit in region.exits: self.assertEqual(exit.parent_region, region) - def testStageMethods(self): + def test_stage_methods(self): """Tests that worlds don't try to implement certain steps that are only ever called as stage.""" for game_name, world_type in AutoWorldRegister.world_types.items(): if not world_type.hidden: diff --git a/test/general/TestItems.py b/test/general/test_items.py similarity index 88% rename from test/general/TestItems.py rename to test/general/test_items.py index 95eb8d28d9..464d246e1f 100644 --- a/test/general/TestItems.py +++ b/test/general/test_items.py @@ -4,7 +4,8 @@ from . import setup_solo_multiworld class TestBase(unittest.TestCase): - def testCreateItem(self): + def test_create_item(self): + """Test that a world can successfully create all items in its datapackage""" for game_name, world_type in AutoWorldRegister.world_types.items(): proxy_world = world_type(None, 0) # this is identical to MultiServer.py creating worlds for item_name in world_type.item_name_to_id: @@ -12,7 +13,7 @@ class TestBase(unittest.TestCase): item = proxy_world.create_item(item_name) self.assertEqual(item.name, item_name) - def testItemNameGroupHasValidItem(self): + def test_item_name_group_has_valid_item(self): """Test that all item name groups contain valid items. """ # This cannot test for Event names that you may have declared for logic, only sendable Items. # In such a case, you can add your entries to this Exclusion dict. Game Name -> Group Names @@ -33,7 +34,7 @@ class TestBase(unittest.TestCase): for item in items: self.assertIn(item, world_type.item_name_to_id) - def testItemNameGroupConflict(self): + def test_item_name_group_conflict(self): """Test that all item name groups aren't also item names.""" for game_name, world_type in AutoWorldRegister.world_types.items(): with self.subTest(game_name, game_name=game_name): @@ -41,7 +42,8 @@ class TestBase(unittest.TestCase): with self.subTest(group_name, group_name=group_name): self.assertNotIn(group_name, world_type.item_name_to_id) - def testItemCountGreaterEqualLocations(self): + def test_item_count_greater_equal_locations(self): + """Test that by the pre_fill step under default settings, each game submits items >= locations""" for game_name, world_type in AutoWorldRegister.world_types.items(): with self.subTest("Game", game=game_name): multiworld = setup_solo_multiworld(world_type) diff --git a/test/general/TestLocations.py b/test/general/test_locations.py similarity index 96% rename from test/general/TestLocations.py rename to test/general/test_locations.py index e77e7a6332..2e609a756f 100644 --- a/test/general/TestLocations.py +++ b/test/general/test_locations.py @@ -5,7 +5,7 @@ from . import setup_solo_multiworld class TestBase(unittest.TestCase): - def testCreateDuplicateLocations(self): + def test_create_duplicate_locations(self): """Tests that no two Locations share a name or ID.""" for game_name, world_type in AutoWorldRegister.world_types.items(): multiworld = setup_solo_multiworld(world_type) @@ -20,7 +20,7 @@ class TestBase(unittest.TestCase): self.assertLessEqual(locations.most_common(1)[0][1], 1, f"{world_type.game} has duplicate of location ID {locations.most_common(1)}") - def testLocationsInDatapackage(self): + def test_locations_in_datapackage(self): """Tests that created locations not filled before fill starts exist in the datapackage.""" for game_name, world_type in AutoWorldRegister.world_types.items(): with self.subTest("Game", game_name=game_name): @@ -30,7 +30,7 @@ class TestBase(unittest.TestCase): self.assertIn(location.name, world_type.location_name_to_id) self.assertEqual(location.address, world_type.location_name_to_id[location.name]) - def testLocationCreationSteps(self): + def test_location_creation_steps(self): """Tests that Regions and Locations aren't created after `create_items`.""" gen_steps = ("generate_early", "create_regions", "create_items") for game_name, world_type in AutoWorldRegister.world_types.items(): @@ -60,7 +60,7 @@ class TestBase(unittest.TestCase): self.assertGreaterEqual(location_count, len(multiworld.get_locations()), f"{game_name} modified locations count during pre_fill") - def testLocationGroup(self): + def test_location_group(self): """Test that all location name groups contain valid locations and don't share names.""" for game_name, world_type in AutoWorldRegister.world_types.items(): with self.subTest(game_name, game_name=game_name): diff --git a/test/general/TestNames.py b/test/general/test_names.py similarity index 92% rename from test/general/TestNames.py rename to test/general/test_names.py index 6dae53240d..7be76eed4b 100644 --- a/test/general/TestNames.py +++ b/test/general/test_names.py @@ -3,7 +3,7 @@ from worlds.AutoWorld import AutoWorldRegister class TestNames(unittest.TestCase): - def testItemNamesFormat(self): + def test_item_names_format(self): """Item names must not be all numeric in order to differentiate between ID and name in !hint""" for gamename, world_type in AutoWorldRegister.world_types.items(): with self.subTest(game=gamename): @@ -11,7 +11,7 @@ class TestNames(unittest.TestCase): self.assertFalse(item_name.isnumeric(), f"Item name \"{item_name}\" is invalid. It must not be numeric.") - def testLocationNameFormat(self): + def test_location_name_format(self): """Location names must not be all numeric in order to differentiate between ID and name in !hint_location""" for gamename, world_type in AutoWorldRegister.world_types.items(): with self.subTest(game=gamename): diff --git a/test/general/TestOptions.py b/test/general/test_options.py similarity index 78% rename from test/general/TestOptions.py rename to test/general/test_options.py index 4a3bd0b02a..e1136f93c9 100644 --- a/test/general/TestOptions.py +++ b/test/general/test_options.py @@ -3,7 +3,8 @@ from worlds.AutoWorld import AutoWorldRegister class TestOptions(unittest.TestCase): - def testOptionsHaveDocString(self): + def test_options_have_doc_string(self): + """Test that submitted options have their own specified docstring""" for gamename, world_type in AutoWorldRegister.world_types.items(): if not world_type.hidden: for option_key, option in world_type.options_dataclass.type_hints.items(): diff --git a/test/general/TestReachability.py b/test/general/test_reachability.py similarity index 91% rename from test/general/TestReachability.py rename to test/general/test_reachability.py index dd786b8352..828912ee35 100644 --- a/test/general/TestReachability.py +++ b/test/general/test_reachability.py @@ -31,7 +31,8 @@ class TestBase(unittest.TestCase): } } - def testDefaultAllStateCanReachEverything(self): + def test_default_all_state_can_reach_everything(self): + """Ensure all state can reach everything and complete the game with the defined options""" for game_name, world_type in AutoWorldRegister.world_types.items(): unreachable_regions = self.default_settings_unreachable_regions.get(game_name, set()) with self.subTest("Game", game=game_name): @@ -54,7 +55,8 @@ class TestBase(unittest.TestCase): with self.subTest("Completion Condition"): self.assertTrue(world.can_beat_game(state)) - def testDefaultEmptyStateCanReachSomething(self): + def test_default_empty_state_can_reach_something(self): + """Ensure empty state can reach at least one location with the defined options""" for game_name, world_type in AutoWorldRegister.world_types.items(): with self.subTest("Game", game=game_name): world = setup_solo_multiworld(world_type) diff --git a/test/netutils/TestLocationStore.py b/test/netutils/test_location_store.py similarity index 100% rename from test/netutils/TestLocationStore.py rename to test/netutils/test_location_store.py diff --git a/test/programs/data/OnePlayer/test.yaml b/test/programs/data/one_player/test.yaml similarity index 100% rename from test/programs/data/OnePlayer/test.yaml rename to test/programs/data/one_player/test.yaml diff --git a/test/programs/TestGenerate.py b/test/programs/test_generate.py similarity index 98% rename from test/programs/TestGenerate.py rename to test/programs/test_generate.py index 73e1d3b834..887a417ec9 100644 --- a/test/programs/TestGenerate.py +++ b/test/programs/test_generate.py @@ -16,7 +16,7 @@ class TestGenerateMain(unittest.TestCase): generate_dir = Path(Generate.__file__).parent run_dir = generate_dir / "test" # reproducible cwd that's neither __file__ nor Generate.__file__ - abs_input_dir = Path(__file__).parent / 'data' / 'OnePlayer' + abs_input_dir = Path(__file__).parent / 'data' / 'one_player' rel_input_dir = abs_input_dir.relative_to(run_dir) # directly supplied relative paths are relative to cwd yaml_input_dir = abs_input_dir.relative_to(generate_dir) # yaml paths are relative to user_path diff --git a/test/programs/TestMultiServer.py b/test/programs/test_multi_server.py similarity index 100% rename from test/programs/TestMultiServer.py rename to test/programs/test_multi_server.py diff --git a/test/utils/TestSIPrefix.py b/test/utils/test_si_prefix.py similarity index 100% rename from test/utils/TestSIPrefix.py rename to test/utils/test_si_prefix.py diff --git a/test/webhost/TestAPIGenerate.py b/test/webhost/test_api_generate.py similarity index 93% rename from test/webhost/TestAPIGenerate.py rename to test/webhost/test_api_generate.py index 8ea78f27f9..b8bdcb38c7 100644 --- a/test/webhost/TestAPIGenerate.py +++ b/test/webhost/test_api_generate.py @@ -19,11 +19,11 @@ class TestDocs(unittest.TestCase): cls.client = app.test_client() - def testCorrectErrorEmptyRequest(self): + def test_correct_error_empty_request(self): response = self.client.post("/api/generate") self.assertIn("No options found. Expected file attachment or json weights.", response.text) - def testGenerationQueued(self): + def test_generation_queued(self): options = { "Tester1": { diff --git a/test/webhost/TestDocs.py b/test/webhost/test_docs.py similarity index 96% rename from test/webhost/TestDocs.py rename to test/webhost/test_docs.py index f6ede1543e..68aba05f9d 100644 --- a/test/webhost/TestDocs.py +++ b/test/webhost/test_docs.py @@ -11,7 +11,7 @@ class TestDocs(unittest.TestCase): def setUpClass(cls) -> None: cls.tutorials_data = WebHost.create_ordered_tutorials_file() - def testHasTutorial(self): + def test_has_tutorial(self): games_with_tutorial = set(entry["gameTitle"] for entry in self.tutorials_data) for game_name, world_type in AutoWorldRegister.world_types.items(): if not world_type.hidden: @@ -27,7 +27,7 @@ class TestDocs(unittest.TestCase): self.fail(f"{game_name} has no setup tutorial. " f"Games with Tutorial: {games_with_tutorial}") - def testHasGameInfo(self): + def test_has_game_info(self): for game_name, world_type in AutoWorldRegister.world_types.items(): if not world_type.hidden: target_path = Utils.local_path("WebHostLib", "static", "generated", "docs", game_name) diff --git a/test/webhost/TestFileGeneration.py b/test/webhost/test_file_generation.py similarity index 96% rename from test/webhost/TestFileGeneration.py rename to test/webhost/test_file_generation.py index f01b70e14f..059f6b49a1 100644 --- a/test/webhost/TestFileGeneration.py +++ b/test/webhost/test_file_generation.py @@ -13,7 +13,7 @@ class TestFileGeneration(unittest.TestCase): # should not create the folder *here* cls.incorrect_path = os.path.join(os.path.split(os.path.dirname(__file__))[0], "WebHostLib") - def testOptions(self): + def test_options(self): from WebHostLib.options import create as create_options_files create_options_files() target = os.path.join(self.correct_path, "static", "generated", "configs") @@ -30,7 +30,7 @@ class TestFileGeneration(unittest.TestCase): for value in roll_options({file.name: f.read()})[0].values(): self.assertTrue(value is True, f"Default Options for template {file.name} cannot be run.") - def testTutorial(self): + def test_tutorial(self): WebHost.create_ordered_tutorials_file() self.assertTrue(os.path.exists(os.path.join(self.correct_path, "static", "generated", "tutorials.json"))) self.assertFalse(os.path.exists(os.path.join(self.incorrect_path, "static", "generated", "tutorials.json"))) From 50244342d9b64bfd0c8534da181c0aa5f292aaed Mon Sep 17 00:00:00 2001 From: BootsinSoots <102177943+BootsinSoots@users.noreply.github.com> Date: Sun, 22 Oct 2023 07:11:19 -0400 Subject: [PATCH 29/54] Docs: Added Note Explaining BK and fix typo in advanced settings (#2316) * Added Note Explaining BK Added suggested change regarding BK mode from Issue #2295 * Changed to Glossary hyperlink * Fix minor typo in exclude_locations * Update worlds/generic/docs/advanced_settings_en.md Co-authored-by: kindasneaki * Docs: Reformat advanced_settings_en/progression_balancing --------- Co-authored-by: kindasneaki Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com> --- worlds/generic/docs/advanced_settings_en.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/worlds/generic/docs/advanced_settings_en.md b/worlds/generic/docs/advanced_settings_en.md index 456795dac4..6d5e20462f 100644 --- a/worlds/generic/docs/advanced_settings_en.md +++ b/worlds/generic/docs/advanced_settings_en.md @@ -108,7 +108,9 @@ guide: [Archipelago Plando Guide](/tutorial/Archipelago/plando/en) * `minimal` will only guarantee that the seed is beatable. You will be guaranteed able to finish the seed logically but may not be able to access all locations or acquire all items. A good example of this is having a big key in the big chest in a dungeon in ALTTP making it impossible to get and finish the dungeon. -* `progression_balancing` is a system the Archipelago generator uses to try and reduce "BK mode" as much as possible. +* `progression_balancing` is a system the Archipelago generator uses to try and reduce + ["BK mode"](/glossary/en/#burger-king-/-bk-mode) + as much as possible. This primarily involves moving necessary progression items into earlier logic spheres to make the games more accessible so that players almost always have something to do. This can be in a range from 0 to 99, and is 50 by default. This number represents a percentage of the furthest progressible player. @@ -130,7 +132,7 @@ guide: [Archipelago Plando Guide](/tutorial/Archipelago/plando/en) there without using any hint points. * `exclude_locations` lets you define any locations that you don't want to do and during generation will force a "junk" item which isn't necessary for progression to go in these locations. -* `priority_locations` is the inverse of `exlcude_locations`, forcing a progression item in the defined locations. +* `priority_locations` is the inverse of `exclude_locations`, forcing a progression item in the defined locations. * `item_links` allows players to link their items into a group with the same item link name and game. The items declared in `item_pool` get combined and when an item is found for the group, all players in the group receive it. Item links can also have local and non local items, forcing the items to either be placed within the worlds of the group or in From 724999fc43c377161c96dee553f362a551b58c22 Mon Sep 17 00:00:00 2001 From: espeon65536 <81029175+espeon65536@users.noreply.github.com> Date: Sun, 22 Oct 2023 10:38:47 -0600 Subject: [PATCH 30/54] Ocarina of Time: long-awaited bugfixes (#2344) - Added location name groups, so you can make your entire Water Temple priority to annoy everyone else - Significant improvement to ER generation success rate (~80% to >99%) - Changed `adult_trade_start` option to a choice option instead of a list (this shouldn't actually break any YAMLs though, due to the lesser-known property of lists parsing as a uniformly-weighted choice) - Major improvements to the option tooltips where needed. (Possibly too much text now) - Changed default hint distribution to `async` to help people's generation times. The tooltip explains that it removes WOTH hints so people hopefully don't get tripped up. - Makes stick and nut capacity upgrades useful items - Added shop prices and required trials to spoiler log - Added Cojiro to adult trade item group, because it had been forgotten previously - Fixed size-modified chests not being moved properly due to trap appearance changing the size - Fixed Thieves Hideout keyring not being allowed in start inventory - Fixed hint generation not accurately flagging barren locations on certain dungeon item shuffle settings - Fixed bug where you could plando arbitrarily-named items into the world, breaking everything --- worlds/oot/ItemPool.py | 4 +- worlds/oot/Location.py | 58 ++++- worlds/oot/LocationList.py | 2 +- worlds/oot/Options.py | 489 ++++++++++++++++++++++--------------- worlds/oot/Patches.py | 30 ++- worlds/oot/Rules.py | 3 - worlds/oot/__init__.py | 90 ++++--- 7 files changed, 424 insertions(+), 252 deletions(-) diff --git a/worlds/oot/ItemPool.py b/worlds/oot/ItemPool.py index 94e1011ddc..6ca6bc9268 100644 --- a/worlds/oot/ItemPool.py +++ b/worlds/oot/ItemPool.py @@ -350,7 +350,7 @@ def generate_itempool(ootworld): ootworld.itempool = [ootworld.create_item(item) for item in pool] for (location_name, item) in placed_items.items(): location = world.get_location(location_name, player) - location.place_locked_item(ootworld.create_item(item)) + location.place_locked_item(ootworld.create_item(item, allow_arbitrary_name=True)) def get_pool_core(world): @@ -675,7 +675,7 @@ def get_pool_core(world): world.remove_from_start_inventory.append('Scarecrow Song') if world.no_epona_race: - world.multiworld.push_precollected(world.create_item('Epona')) + world.multiworld.push_precollected(world.create_item('Epona', allow_arbitrary_name=True)) world.remove_from_start_inventory.append('Epona') if world.shuffle_smallkeys == 'vanilla': diff --git a/worlds/oot/Location.py b/worlds/oot/Location.py index e2b0e52e4d..3f7d75517e 100644 --- a/worlds/oot/Location.py +++ b/worlds/oot/Location.py @@ -2,6 +2,8 @@ from enum import Enum from .LocationList import location_table from BaseClasses import Location +non_indexed_location_types = {'Boss', 'Event', 'Drop', 'HintStone', 'Hint'} + location_id_offset = 67000 locnames_pre_70 = { "Gift from Sages", @@ -18,7 +20,7 @@ new_name_order = sorted(location_table.keys(), else 0) location_name_to_id = {name: (location_id_offset + index) for (index, name) in enumerate(new_name_order) - if location_table[name][0] not in {'Boss', 'Event', 'Drop', 'HintStone', 'Hint'}} + if location_table[name][0] not in non_indexed_location_types} class DisableType(Enum): ENABLED = 0 @@ -83,3 +85,57 @@ def LocationFactory(locations, player: int): return ret +def build_location_name_groups() -> dict: + + def fix_sing(t) -> tuple: + if isinstance(t, str): + return (t,) + return t + + def rename(d, k1, k2) -> None: + d[k2] = d[k1] + del d[k1] + + # whoever wrote the location table didn't realize they need to add a comma to mark a singleton as a tuple + # so we have to check types unfortunately + tags = set() + for v in location_table.values(): + if v[5] is not None: + tags.update(fix_sing(v[5])) + + sorted_tags = sorted(list(tags)) + + ret = { + tag: {k for k, v in location_table.items() + if v[5] is not None + and tag in fix_sing(v[5]) + and v[0] not in non_indexed_location_types} + for tag in sorted_tags + } + + # Delete tags which are a combination of other tags + del ret['Death Mountain'] + del ret['Forest'] + del ret['Gerudo'] + del ret['Kakariko'] + del ret['Market'] + + # Delete Vanilla and MQ tags because they are just way too broad + del ret['Vanilla'] + del ret['Master Quest'] + + rename(ret, 'Beehive', 'Beehives') + rename(ret, 'Cow', 'Cows') + rename(ret, 'Crate', 'Crates') + rename(ret, 'Deku Scrub', 'Deku Scrubs') + rename(ret, 'FlyingPot', 'Flying Pots') + rename(ret, 'Freestanding', 'Freestanding Items') + rename(ret, 'Pot', 'Pots') + rename(ret, 'RupeeTower', 'Rupee Groups') + rename(ret, 'SmallCrate', 'Small Crates') + rename(ret, 'the Market', 'Market') + rename(ret, 'the Graveyard', 'Graveyard') + rename(ret, 'the Lost Woods', 'Lost Woods') + + return ret + diff --git a/worlds/oot/LocationList.py b/worlds/oot/LocationList.py index 3f4602c428..27ad575699 100644 --- a/worlds/oot/LocationList.py +++ b/worlds/oot/LocationList.py @@ -238,7 +238,7 @@ location_table = OrderedDict([ ("Market Night Green Rupee Crate 1", ("Crate", 0x21, (0,0,24), None, 'Rupee (1)', ("the Market", "Market", "Crate"))), ("Market Night Green Rupee Crate 2", ("Crate", 0x21, (0,0,25), None, 'Rupee (1)', ("the Market", "Market", "Crate"))), ("Market Night Green Rupee Crate 3", ("Crate", 0x21, (0,0,26), None, 'Rupee (1)', ("the Market", "Market", "Crate"))), - ("Market Dog Lady House Crate", ("Crate", 0x35, (0,0,3), None, 'Rupees (5)', ("Market", "Market", "Crate"))), + ("Market Dog Lady House Crate", ("Crate", 0x35, (0,0,3), None, 'Rupees (5)', ("the Market", "Market", "Crate"))), ("Market Guard House Child Crate", ("Crate", 0x4D, (0,0,6), None, 'Rupee (1)', ("the Market", "Market", "Crate"))), ("Market Guard House Child Pot 1", ("Pot", 0x4D, (0,0,9), None, 'Rupee (1)', ("the Market", "Market", "Pot"))), ("Market Guard House Child Pot 2", ("Pot", 0x4D, (0,0,10), None, 'Rupee (1)', ("the Market", "Market", "Pot"))), diff --git a/worlds/oot/Options.py b/worlds/oot/Options.py index 03f5346cee..120027e29d 100644 --- a/worlds/oot/Options.py +++ b/worlds/oot/Options.py @@ -30,7 +30,17 @@ class TrackRandomRange(Range): class Logic(Choice): - """Set the logic used for the generator.""" + """Set the logic used for the generator. + Glitchless: Normal gameplay. Can enable more difficult logical paths using the Logic Tricks option. + Glitched: Many powerful glitches expected, such as bomb hovering and clipping. + Glitched is incompatible with the following settings: + - All forms of entrance randomizer + - MQ dungeons + - Pot shuffle + - Freestanding item shuffle + - Crate shuffle + - Beehive shuffle + No Logic: No logic is used when placing items. Not recommended for most players.""" display_name = "Logic Rules" option_glitchless = 0 option_glitched = 1 @@ -38,12 +48,16 @@ class Logic(Choice): class NightTokens(Toggle): - """Nighttime skulltulas will logically require Sun's Song.""" + """When enabled, nighttime skulltulas logically require Sun's Song.""" display_name = "Nighttime Skulltulas Expect Sun's Song" class Forest(Choice): - """Set the state of Kokiri Forest and the path to Deku Tree.""" + """Set the state of Kokiri Forest and the path to Deku Tree. + Open: Neither the forest exit nor the path to Deku Tree is blocked. + Closed Deku: The forest exit is not blocked; the path to Deku Tree requires Kokiri Sword and Deku Shield. + Closed: Path to Deku Tree requires sword and shield. The forest exit is blocked until Deku Tree is beaten. + Closed forest will force child start, and becomes Closed Deku if interior entrances, overworld entrances, warp songs, or random spawn positions are enabled.""" display_name = "Forest" option_open = 0 option_closed_deku = 1 @@ -53,7 +67,10 @@ class Forest(Choice): class Gate(Choice): - """Set the state of the Kakariko Village gate.""" + """Set the state of the Kakariko Village gate for child. The gate is always open as adult. + Open: The gate starts open. Happy Mask Shop opens upon receiving Zelda's Letter. + Zelda: The gate and Mask Shop open upon receiving Zelda's Letter, without needing to show it to the guard. + Closed: Vanilla behavior; the gate and Mask Shop open upon showing Zelda's Letter to the gate guard.""" display_name = "Kakariko Gate" option_open = 0 option_zelda = 1 @@ -61,12 +78,15 @@ class Gate(Choice): class DoorOfTime(DefaultOnToggle): - """Open the Door of Time by default, without the Song of Time.""" + """When enabled, the Door of Time starts opened, without needing Song of Time.""" display_name = "Open Door of Time" class Fountain(Choice): - """Set the state of King Zora, blocking the way to Zora's Fountain.""" + """Set the state of King Zora, blocking the way to Zora's Fountain. + Open: King Zora starts moved as both ages. Ruto's Letter is removed. + Adult: King Zora must be moved as child, but is always moved for adult. + Closed: Vanilla behavior; King Zora must be shown Ruto's Letter as child to move him as both ages.""" display_name = "Zora's Fountain" option_open = 0 option_adult = 1 @@ -75,7 +95,10 @@ class Fountain(Choice): class Fortress(Choice): - """Set the requirements for access to Gerudo Fortress.""" + """Set the requirements for access to Gerudo Fortress. + Normal: Vanilla behavior; all four carpenters must be rescued. + Fast: Only one carpenter must be rescued, which is the one in the bottom-left of the fortress. + Open: The Gerudo Valley bridge starts repaired. Gerudo Membership Card is given to start if not shuffled.""" display_name = "Gerudo Fortress" option_normal = 0 option_fast = 1 @@ -84,7 +107,14 @@ class Fortress(Choice): class Bridge(Choice): - """Set the requirements for the Rainbow Bridge.""" + """Set the requirements for the Rainbow Bridge. + Open: The bridge is always present. + Vanilla: Bridge requires Shadow Medallion, Spirit Medallion, and Light Arrows. + Stones: Bridge requires a configurable amount of Spiritual Stones. + Medallions: Bridge requires a configurable amount of medallions. + Dungeons: Bridge requires a configurable amount of rewards (stones + medallions). + Tokens: Bridge requires a configurable amount of gold skulltula tokens. + Hearts: Bridge requires a configurable amount of hearts.""" display_name = "Rainbow Bridge Requirement" option_open = 0 option_vanilla = 1 @@ -122,8 +152,9 @@ class StartingAge(Choice): class InteriorEntrances(Choice): - """Shuffles interior entrances. "Simple" shuffles houses and Great Fairies; "All" includes Windmill, Link's House, - Temple of Time, and Kak potion shop.""" + """Shuffles interior entrances. + Simple: Houses and Great Fairies are shuffled. + All: In addition to Simple, includes Windmill, Link's House, Temple of Time, and the Kakariko potion shop.""" display_name = "Shuffle Interior Entrances" option_off = 0 option_simple = 1 @@ -137,7 +168,9 @@ class GrottoEntrances(Toggle): class DungeonEntrances(Choice): - """Shuffles dungeon entrances. Opens Deku, Fire and BotW to both ages. "All" includes Ganon's Castle.""" + """Shuffles dungeon entrances. When enabled, both ages will have access to Fire Temple, Bottom of the Well, and Deku Tree. + Simple: Shuffle dungeon entrances except for Ganon's Castle. + All: Include Ganon's Castle as well.""" display_name = "Shuffle Dungeon Entrances" option_off = 0 option_simple = 1 @@ -146,7 +179,9 @@ class DungeonEntrances(Choice): class BossEntrances(Choice): - """Shuffles boss entrances. "Limited" prevents age-mixing of bosses.""" + """Shuffles boss entrances. + Limited: Bosses will be limited to the ages that typically fight them. + Full: Bosses may be fought as different ages than usual. Child can defeat Phantom Ganon and Bongo Bongo.""" display_name = "Shuffle Boss Entrances" option_off = 0 option_limited = 1 @@ -178,19 +213,19 @@ class SpawnPositions(Choice): alias_true = 3 -class MixEntrancePools(Choice): - """Shuffles entrances into a mixed pool instead of separate ones. "indoor" keeps overworld entrances separate; "all" - mixes them in.""" - display_name = "Mix Entrance Pools" - option_off = 0 - option_indoor = 1 - option_all = 2 +# class MixEntrancePools(Choice): +# """Shuffles entrances into a mixed pool instead of separate ones. "indoor" keeps overworld entrances separate; "all" +# mixes them in.""" +# display_name = "Mix Entrance Pools" +# option_off = 0 +# option_indoor = 1 +# option_all = 2 -class DecoupleEntrances(Toggle): - """Decouple entrances when shuffling them. Also adds the one-way entrance from Gerudo Valley to Lake Hylia if - overworld is shuffled.""" - display_name = "Decouple Entrances" +# class DecoupleEntrances(Toggle): +# """Decouple entrances when shuffling them. Also adds the one-way entrance from Gerudo Valley to Lake Hylia if +# overworld is shuffled.""" +# display_name = "Decouple Entrances" class TriforceHunt(Toggle): @@ -216,13 +251,17 @@ class ExtraTriforces(Range): class LogicalChus(Toggle): - """Bombchus are properly considered in logic. The first found pack will have 20 chus; Kokiri Shop and Bazaar sell - refills; bombchus open Bombchu Bowling.""" + """Bombchus are properly considered in logic. + The first found pack will always have 20 chus. + Kokiri Shop and Bazaar will sell refills at reduced cost. + Bombchus open Bombchu Bowling.""" display_name = "Bombchus Considered in Logic" class DungeonShortcuts(Choice): - """Shortcuts to dungeon bosses are available without any requirements.""" + """Shortcuts to dungeon bosses are available without any requirements. + If enabled, this will impact the logic of dungeons where shortcuts are available. + Choice: Use the option "dungeon_shortcuts_list" to choose shortcuts.""" display_name = "Dungeon Boss Shortcuts Mode" option_off = 0 option_choice = 1 @@ -246,7 +285,11 @@ class DungeonShortcutsList(OptionSet): class MQDungeons(Choice): - """Choose between vanilla and Master Quest dungeon layouts.""" + """Choose between vanilla and Master Quest dungeon layouts. + Vanilla: All layouts are vanilla. + MQ: All layouts are Master Quest. + Specific: Use the option "mq_dungeons_list" to choose which dungeons are MQ. + Count: Use the option "mq_dungeons_count" to choose a number of random dungeons as MQ.""" display_name = "MQ Dungeon Mode" option_vanilla = 0 option_mq = 1 @@ -255,7 +298,7 @@ class MQDungeons(Choice): class MQDungeonList(OptionSet): - """Chosen dungeons to be MQ layout.""" + """With MQ dungeons as Specific: chosen dungeons to be MQ layout.""" display_name = "MQ Dungeon List" valid_keys = { "Deku Tree", @@ -274,41 +317,41 @@ class MQDungeonList(OptionSet): class MQDungeonCount(TrackRandomRange): - """Number of MQ dungeons, chosen randomly.""" + """With MQ dungeons as Count: number of randomly-selected dungeons to be MQ layout.""" display_name = "MQ Dungeon Count" range_start = 0 range_end = 12 default = 0 -class EmptyDungeons(Choice): - """Pre-completed dungeons are barren and rewards are given for free.""" - display_name = "Pre-completed Dungeons Mode" - option_none = 0 - option_specific = 1 - option_count = 2 +# class EmptyDungeons(Choice): +# """Pre-completed dungeons are barren and rewards are given for free.""" +# display_name = "Pre-completed Dungeons Mode" +# option_none = 0 +# option_specific = 1 +# option_count = 2 -class EmptyDungeonList(OptionSet): - """Chosen dungeons to be pre-completed.""" - display_name = "Pre-completed Dungeon List" - valid_keys = { - "Deku Tree", - "Dodongo's Cavern", - "Jabu Jabu's Belly", - "Forest Temple", - "Fire Temple", - "Water Temple", - "Shadow Temple", - "Spirit Temple", - } +# class EmptyDungeonList(OptionSet): +# """Chosen dungeons to be pre-completed.""" +# display_name = "Pre-completed Dungeon List" +# valid_keys = { +# "Deku Tree", +# "Dodongo's Cavern", +# "Jabu Jabu's Belly", +# "Forest Temple", +# "Fire Temple", +# "Water Temple", +# "Shadow Temple", +# "Spirit Temple", +# } -class EmptyDungeonCount(Range): - display_name = "Pre-completed Dungeon Count" - range_start = 1 - range_end = 8 - default = 2 +# class EmptyDungeonCount(Range): +# display_name = "Pre-completed Dungeon Count" +# range_start = 1 +# range_end = 8 +# default = 2 world_options: typing.Dict[str, type(Option)] = { @@ -341,59 +384,8 @@ world_options: typing.Dict[str, type(Option)] = { } -# class LacsCondition(Choice): -# """Set the requirements for the Light Arrow Cutscene in the Temple of Time.""" -# display_name = "Light Arrow Cutscene Requirement" -# option_vanilla = 0 -# option_stones = 1 -# option_medallions = 2 -# option_dungeons = 3 -# option_tokens = 4 - - -# class LacsStones(Range): -# """Set the number of Spiritual Stones required for LACS.""" -# display_name = "Spiritual Stones Required for LACS" -# range_start = 0 -# range_end = 3 -# default = 3 - - -# class LacsMedallions(Range): -# """Set the number of medallions required for LACS.""" -# display_name = "Medallions Required for LACS" -# range_start = 0 -# range_end = 6 -# default = 6 - - -# class LacsRewards(Range): -# """Set the number of dungeon rewards required for LACS.""" -# display_name = "Dungeon Rewards Required for LACS" -# range_start = 0 -# range_end = 9 -# default = 9 - - -# class LacsTokens(Range): -# """Set the number of Gold Skulltula Tokens required for LACS.""" -# display_name = "Tokens Required for LACS" -# range_start = 0 -# range_end = 100 -# default = 40 - - -# lacs_options: typing.Dict[str, type(Option)] = { -# "lacs_condition": LacsCondition, -# "lacs_stones": LacsStones, -# "lacs_medallions": LacsMedallions, -# "lacs_rewards": LacsRewards, -# "lacs_tokens": LacsTokens, -# } - - class BridgeStones(Range): - """Set the number of Spiritual Stones required for the rainbow bridge.""" + """With Stones bridge: set the number of Spiritual Stones required.""" display_name = "Spiritual Stones Required for Bridge" range_start = 0 range_end = 3 @@ -401,7 +393,7 @@ class BridgeStones(Range): class BridgeMedallions(Range): - """Set the number of medallions required for the rainbow bridge.""" + """With Medallions bridge: set the number of medallions required.""" display_name = "Medallions Required for Bridge" range_start = 0 range_end = 6 @@ -409,7 +401,7 @@ class BridgeMedallions(Range): class BridgeRewards(Range): - """Set the number of dungeon rewards required for the rainbow bridge.""" + """With Dungeons bridge: set the number of dungeon rewards required.""" display_name = "Dungeon Rewards Required for Bridge" range_start = 0 range_end = 9 @@ -417,7 +409,7 @@ class BridgeRewards(Range): class BridgeTokens(Range): - """Set the number of Gold Skulltula Tokens required for the rainbow bridge.""" + """With Tokens bridge: set the number of Gold Skulltula Tokens required.""" display_name = "Tokens Required for Bridge" range_start = 0 range_end = 100 @@ -425,7 +417,7 @@ class BridgeTokens(Range): class BridgeHearts(Range): - """Set the number of hearts required for the rainbow bridge.""" + """With Hearts bridge: set the number of hearts required.""" display_name = "Hearts Required for Bridge" range_start = 4 range_end = 20 @@ -442,7 +434,15 @@ bridge_options: typing.Dict[str, type(Option)] = { class SongShuffle(Choice): - """Set where songs can appear.""" + """Set where songs can appear. + Song: Songs are shuffled into other song locations. + Dungeon: Songs are placed into end-of-dungeon locations: + - The 8 boss heart containers + - Sheik in Ice Cavern + - Lens of Truth chest in Bottom of the Well + - Ice Arrows chest in Gerudo Training Ground + - Impa at Hyrule Castle + Any: Songs can appear anywhere in the multiworld.""" display_name = "Shuffle Songs" option_song = 0 option_dungeon = 1 @@ -450,8 +450,10 @@ class SongShuffle(Choice): class ShopShuffle(Choice): - """Randomizes shop contents. "fixed_number" randomizes a specific number of items per shop; - "random_number" randomizes the value for each shop. """ + """Randomizes shop contents. + Off: Shops are not randomized at all. + Fixed Number: Shop contents are shuffled, and a specific number of multiworld locations exist in each shop, controlled by the "shop_slots" option. + Random Number: Same as Fixed Number, but the number of locations per shop is random and may differ between shops.""" display_name = "Shopsanity" option_off = 0 option_fixed_number = 1 @@ -459,15 +461,20 @@ class ShopShuffle(Choice): class ShopSlots(Range): - """Number of items per shop to be randomized into the main itempool. - Only active if Shopsanity is set to "fixed_number." """ + """With Shopsanity fixed number: quantity of multiworld locations per shop to be randomized.""" display_name = "Shuffled Shop Slots" range_start = 0 range_end = 4 class ShopPrices(Choice): - """Controls prices of shop items. "Normal" is a distribution from 0 to 300. "X Wallet" requires that wallet at max. "Affordable" is always 10 rupees.""" + """Controls prices of shop locations. + Normal: Balanced distribution from 0 to 300. + Affordable: Every shop location costs 10 rupees. + Starting Wallet: Prices capped at 99 rupees. + Adult's Wallet: Prices capped at 200 rupees. + Giant's Wallet: Prices capped at 500 rupees. + Tycoon's Wallet: Prices capped at 999 rupees.""" display_name = "Shopsanity Prices" option_normal = 0 option_affordable = 1 @@ -478,7 +485,10 @@ class ShopPrices(Choice): class TokenShuffle(Choice): - """Token rewards from Gold Skulltulas are shuffled into the pool.""" + """Token rewards from Gold Skulltulas can be shuffled into the pool. + Dungeons: Only skulltulas in dungeons are shuffled. + Overworld: Only skulltulas on the overworld (all skulltulas not in dungeons) are shuffled. + All: Every skulltula is shuffled.""" display_name = "Tokensanity" option_off = 0 option_dungeons = 1 @@ -487,7 +497,11 @@ class TokenShuffle(Choice): class ScrubShuffle(Choice): - """Shuffle the items sold by Business Scrubs, and set the prices.""" + """Shuffle the items sold by Business Scrubs, and set the prices. + Off: Only the three business scrubs that sell one-time upgrades in vanilla will have items at their vanilla prices. + Low/"Affordable": All scrub prices are 10 rupees. + Regular/"Expensive": All scrub prices are vanilla. + Random Prices: All scrub prices are randomized between 0 and 99 rupees.""" display_name = "Scrub Shuffle" option_off = 0 option_low = 1 @@ -513,7 +527,11 @@ class ShuffleOcarinas(Toggle): class ShuffleChildTrade(Choice): - """Controls the behavior of the start of the child trade quest.""" + """Controls the behavior of the start of the child trade quest. + Vanilla: Malon will give you the Weird Egg at Hyrule Castle. + Shuffle: Malon will give you a random item, and the Weird Egg is shuffled. + Skip Child Zelda: The game starts with Zelda already met, Zelda's Letter obtained, and the item from Impa obtained. + """ display_name = "Shuffle Child Trade Item" option_vanilla = 0 option_shuffle = 1 @@ -538,30 +556,39 @@ class ShuffleMedigoronCarpet(Toggle): class ShuffleFreestanding(Choice): - """Shuffles freestanding rupees, recovery hearts, Shadow Temple Spinning Pots, and Goron Pot.""" + """Shuffles freestanding rupees, recovery hearts, Shadow Temple Spinning Pots, and Goron Pot drops. + Dungeons: Only freestanding items in dungeons are shuffled. + Overworld: Only freestanding items in the overworld are shuffled. + All: All freestanding items are shuffled.""" display_name = "Shuffle Rupees & Hearts" option_off = 0 - option_all = 1 + option_dungeons = 1 option_overworld = 2 - option_dungeons = 3 + option_all = 3 class ShufflePots(Choice): - """Shuffles pots and flying pots which normally contain an item.""" + """Shuffles pots and flying pots which normally contain an item. + Dungeons: Only pots in dungeons are shuffled. + Overworld: Only pots in the overworld are shuffled. + All: All pots are shuffled.""" display_name = "Shuffle Pots" option_off = 0 - option_all = 1 + option_dungeons = 1 option_overworld = 2 - option_dungeons = 3 + option_all = 3 class ShuffleCrates(Choice): - """Shuffles large and small crates containing an item.""" + """Shuffles large and small crates containing an item. + Dungeons: Only crates in dungeons are shuffled. + Overworld: Only crates in the overworld are shuffled. + All: All crates are shuffled.""" display_name = "Shuffle Crates" option_off = 0 - option_all = 1 + option_dungeons = 1 option_overworld = 2 - option_dungeons = 3 + option_all = 3 class ShuffleBeehives(Toggle): @@ -597,72 +624,113 @@ shuffle_options: typing.Dict[str, type(Option)] = { class ShuffleMapCompass(Choice): - """Control where to shuffle dungeon maps and compasses.""" + """Control where to shuffle dungeon maps and compasses. + Remove: There will be no maps or compasses in the itempool. + Startwith: You start with all maps and compasses. + Vanilla: Maps and compasses remain vanilla. + Dungeon: Maps and compasses are shuffled within their original dungeon. + Regional: Maps and compasses are shuffled only in regions near the original dungeon. + Overworld: Maps and compasses are shuffled locally outside of dungeons. + Any Dungeon: Maps and compasses are shuffled locally in any dungeon. + Keysanity: Maps and compasses can be anywhere in the multiworld.""" display_name = "Maps & Compasses" option_remove = 0 option_startwith = 1 option_vanilla = 2 option_dungeon = 3 - option_overworld = 4 - option_any_dungeon = 5 - option_keysanity = 6 - option_regional = 7 + option_regional = 4 + option_overworld = 5 + option_any_dungeon = 6 + option_keysanity = 7 default = 1 - alias_anywhere = 6 + alias_anywhere = 7 class ShuffleKeys(Choice): - """Control where to shuffle dungeon small keys.""" + """Control where to shuffle dungeon small keys. + Remove/"Keysy": There will be no small keys in the itempool. All small key doors are automatically unlocked. + Vanilla: Small keys remain vanilla. You may start with extra small keys in some dungeons to prevent softlocks. + Dungeon: Small keys are shuffled within their original dungeon. + Regional: Small keys are shuffled only in regions near the original dungeon. + Overworld: Small keys are shuffled locally outside of dungeons. + Any Dungeon: Small keys are shuffled locally in any dungeon. + Keysanity: Small keys can be anywhere in the multiworld.""" display_name = "Small Keys" option_remove = 0 option_vanilla = 2 option_dungeon = 3 - option_overworld = 4 - option_any_dungeon = 5 - option_keysanity = 6 - option_regional = 7 + option_regional = 4 + option_overworld = 5 + option_any_dungeon = 6 + option_keysanity = 7 default = 3 alias_keysy = 0 - alias_anywhere = 6 + alias_anywhere = 7 class ShuffleGerudoKeys(Choice): - """Control where to shuffle the Thieves' Hideout small keys.""" + """Control where to shuffle the Thieves' Hideout small keys. + Vanilla: Hideout keys remain vanilla. + Regional: Hideout keys are shuffled only in the Gerudo Valley/Desert Colossus area. + Overworld: Hideout keys are shuffled locally outside of dungeons. + Any Dungeon: Hideout keys are shuffled locally in any dungeon. + Keysanity: Hideout keys can be anywhere in the multiworld.""" display_name = "Thieves' Hideout Keys" option_vanilla = 0 - option_overworld = 1 - option_any_dungeon = 2 - option_keysanity = 3 - option_regional = 4 - alias_anywhere = 3 + option_regional = 1 + option_overworld = 2 + option_any_dungeon = 3 + option_keysanity = 4 + alias_anywhere = 4 class ShuffleBossKeys(Choice): - """Control where to shuffle boss keys, except the Ganon's Castle Boss Key.""" + """Control where to shuffle boss keys, except the Ganon's Castle Boss Key. + Remove/"Keysy": There will be no boss keys in the itempool. All boss key doors are automatically unlocked. + Vanilla: Boss keys remain vanilla. You may start with extra small keys in some dungeons to prevent softlocks. + Dungeon: Boss keys are shuffled within their original dungeon. + Regional: Boss keys are shuffled only in regions near the original dungeon. + Overworld: Boss keys are shuffled locally outside of dungeons. + Any Dungeon: Boss keys are shuffled locally in any dungeon. + Keysanity: Boss keys can be anywhere in the multiworld.""" display_name = "Boss Keys" option_remove = 0 option_vanilla = 2 option_dungeon = 3 - option_overworld = 4 - option_any_dungeon = 5 - option_keysanity = 6 - option_regional = 7 + option_regional = 4 + option_overworld = 5 + option_any_dungeon = 6 + option_keysanity = 7 default = 3 alias_keysy = 0 - alias_anywhere = 6 + alias_anywhere = 7 class ShuffleGanonBK(Choice): - """Control how to shuffle the Ganon's Castle Boss Key.""" + """Control how to shuffle the Ganon's Castle Boss Key (GCBK). + Remove: GCBK is removed, and the boss key door is automatically unlocked. + Vanilla: GCBK remains vanilla. + Dungeon: GCBK is shuffled within its original dungeon. + Regional: GCBK is shuffled only in Hyrule Field, Market, and Hyrule Castle areas. + Overworld: GCBK is shuffled locally outside of dungeons. + Any Dungeon: GCBK is shuffled locally in any dungeon. + Keysanity: GCBK can be anywhere in the multiworld. + On LACS: GCBK is on the Light Arrow Cutscene, which requires Shadow and Spirit Medallions. + Stones: GCBK will be awarded when reaching the target number of Spiritual Stones. + Medallions: GCBK will be awarded when reaching the target number of medallions. + Dungeons: GCBK will be awarded when reaching the target number of dungeon rewards. + Tokens: GCBK will be awarded when reaching the target number of Gold Skulltula Tokens. + Hearts: GCBK will be awarded when reaching the target number of hearts. + """ display_name = "Ganon's Boss Key" option_remove = 0 option_vanilla = 2 option_dungeon = 3 - option_overworld = 4 - option_any_dungeon = 5 - option_keysanity = 6 - option_on_lacs = 7 - option_regional = 8 + option_regional = 4 + option_overworld = 5 + option_any_dungeon = 6 + option_keysanity = 7 + option_on_lacs = 8 option_stones = 9 option_medallions = 10 option_dungeons = 11 @@ -670,7 +738,7 @@ class ShuffleGanonBK(Choice): option_hearts = 13 default = 0 alias_keysy = 0 - alias_anywhere = 6 + alias_anywhere = 7 class EnhanceMC(Toggle): @@ -679,7 +747,7 @@ class EnhanceMC(Toggle): class GanonBKMedallions(Range): - """Set how many medallions are required to receive Ganon BK.""" + """With medallions GCBK: set how many medallions are required to receive GCBK.""" display_name = "Medallions Required for Ganon's BK" range_start = 1 range_end = 6 @@ -687,7 +755,7 @@ class GanonBKMedallions(Range): class GanonBKStones(Range): - """Set how many Spiritual Stones are required to receive Ganon BK.""" + """With stones GCBK: set how many Spiritual Stones are required to receive GCBK.""" display_name = "Spiritual Stones Required for Ganon's BK" range_start = 1 range_end = 3 @@ -695,7 +763,7 @@ class GanonBKStones(Range): class GanonBKRewards(Range): - """Set how many dungeon rewards are required to receive Ganon BK.""" + """With dungeons GCBK: set how many dungeon rewards are required to receive GCBK.""" display_name = "Dungeon Rewards Required for Ganon's BK" range_start = 1 range_end = 9 @@ -703,7 +771,7 @@ class GanonBKRewards(Range): class GanonBKTokens(Range): - """Set how many Gold Skulltula Tokens are required to receive Ganon BK.""" + """With tokens GCBK: set how many Gold Skulltula Tokens are required to receive GCBK.""" display_name = "Tokens Required for Ganon's BK" range_start = 1 range_end = 100 @@ -711,7 +779,7 @@ class GanonBKTokens(Range): class GanonBKHearts(Range): - """Set how many hearts are required to receive Ganon BK.""" + """With hearts GCBK: set how many hearts are required to receive GCBK.""" display_name = "Hearts Required for Ganon's BK" range_start = 4 range_end = 20 @@ -719,7 +787,9 @@ class GanonBKHearts(Range): class KeyRings(Choice): - """Dungeons have all small keys found at once, rather than individually.""" + """A key ring grants all dungeon small keys at once, rather than individually. + Choose: Use the option "key_rings_list" to choose which dungeons have key rings. + All: All dungeons have key rings instead of small keys.""" display_name = "Key Rings Mode" option_off = 0 option_choose = 1 @@ -728,7 +798,7 @@ class KeyRings(Choice): class KeyRingList(OptionSet): - """Select areas with keyrings rather than individual small keys.""" + """With key rings as Choose: select areas with key rings rather than individual small keys.""" display_name = "Key Ring Areas" valid_keys = { "Thieves' Hideout", @@ -828,7 +898,8 @@ class BigPoeCount(Range): class FAETorchCount(Range): - """Number of lit torches required to open Shadow Temple.""" + """Number of lit torches required to open Shadow Temple. + Does not affect logic; use the trick Shadow Temple Entry with Fire Arrows if desired.""" display_name = "Fire Arrow Entry Torch Count" range_start = 1 range_end = 24 @@ -853,7 +924,11 @@ timesavers_options: typing.Dict[str, type(Option)] = { class CorrectChestAppearance(Choice): - """Changes chest textures and/or sizes to match their contents. "Classic" is the old behavior of CSMC.""" + """Changes chest textures and/or sizes to match their contents. + Off: All chests have their vanilla size/appearance. + Textures: Chest textures reflect their contents. + Both: Like Textures, but progression items and boss keys get big chests, and other items get small chests. + Classic: Old behavior of CSMC; textures distinguish keys from non-keys, and size distinguishes importance.""" display_name = "Chest Appearance Matches Contents" option_off = 0 option_textures = 1 @@ -872,15 +947,24 @@ class InvisibleChests(Toggle): class CorrectPotCrateAppearance(Choice): - """Unchecked pots and crates have a different texture; unchecked beehives will wiggle. With textures_content, pots and crates have an appearance based on their contents; with textures_unchecked, all unchecked pots/crates have the same appearance.""" + """Changes the appearance of pots, crates, and beehives that contain items. + Off: Vanilla appearance for all containers. + Textures (Content): Unchecked pots and crates have a texture reflecting their contents. Unchecked beehives with progression items will wiggle. + Textures (Unchecked): Unchecked pots and crates are golden. Unchecked beehives will wiggle. + """ display_name = "Pot, Crate, and Beehive Appearance" option_off = 0 option_textures_content = 1 option_textures_unchecked = 2 + default = 2 class Hints(Choice): - """Gossip Stones can give hints about item locations.""" + """Gossip Stones can give hints about item locations. + None: Gossip Stones do not give hints. + Mask: Gossip Stones give hints with Mask of Truth. + Agony: Gossip Stones give hints wtih Stone of Agony. + Always: Gossip Stones always give hints.""" display_name = "Gossip Stones" option_none = 0 option_mask = 1 @@ -895,7 +979,9 @@ class MiscHints(DefaultOnToggle): class HintDistribution(Choice): - """Choose the hint distribution to use. Affects the frequency of strong hints, which items are always hinted, etc.""" + """Choose the hint distribution to use. Affects the frequency of strong hints, which items are always hinted, etc. + Detailed documentation on hint distributions can be found on the Archipelago GitHub or OoTRandomizer.com. + The Async hint distribution is intended for async multiworlds. It removes Way of the Hero hints to improve generation times, since they are not very useful in asyncs.""" display_name = "Hint Distribution" option_balanced = 0 option_ddr = 1 @@ -907,10 +993,13 @@ class HintDistribution(Choice): option_useless = 7 option_very_strong = 8 option_async = 9 + default = 9 class TextShuffle(Choice): - """Randomizes text in the game for comedic effect.""" + """Randomizes text in the game for comedic effect. + Except Hints: does not randomize important text such as hints, small/boss key information, and item prices. + Complete: randomizes every textbox, including the useful ones.""" display_name = "Text Shuffle" option_none = 0 option_except_hints = 1 @@ -946,7 +1035,8 @@ class HeroMode(Toggle): class StartingToD(Choice): - """Change the starting time of day.""" + """Change the starting time of day. + Daytime starts at Sunrise and ends at Sunset. Default is between Morning and Noon.""" display_name = "Starting Time of Day" option_default = 0 option_sunrise = 1 @@ -999,7 +1089,11 @@ misc_options: typing.Dict[str, type(Option)] = { } class ItemPoolValue(Choice): - """Changes the number of items available in the game.""" + """Changes the number of items available in the game. + Plentiful: One extra copy of every major item. + Balanced: Original item pool. + Scarce: Extra copies of major items are removed. Heart containers are removed. + Minimal: All major item upgrades not used for locations are removed. All health is removed.""" display_name = "Item Pool" option_plentiful = 0 option_balanced = 1 @@ -1009,7 +1103,12 @@ class ItemPoolValue(Choice): class IceTraps(Choice): - """Adds ice traps to the item pool.""" + """Adds ice traps to the item pool. + Off: All ice traps are removed. + Normal: The vanilla quantity of ice traps are placed. + On/"Extra": There is a chance for some extra ice traps to be placed. + Mayhem: All added junk items are ice traps. + Onslaught: All junk items are replaced by ice traps, even those in the base pool.""" display_name = "Ice Traps" option_off = 0 option_normal = 1 @@ -1021,34 +1120,27 @@ class IceTraps(Choice): class IceTrapVisual(Choice): - """Changes the appearance of ice traps as freestanding items.""" - display_name = "Ice Trap Appearance" + """Changes the appearance of traps, including other games' traps, as freestanding items.""" + display_name = "Trap Appearance" option_major_only = 0 option_junk_only = 1 option_anything = 2 -class AdultTradeStart(OptionSet): - """Choose the items that can appear to start the adult trade sequence. By default it is Claim Check only.""" - display_name = "Adult Trade Sequence Items" - default = {"Claim Check"} - valid_keys = { - "Pocket Egg", - "Pocket Cucco", - "Cojiro", - "Odd Mushroom", - "Poachers Saw", - "Broken Sword", - "Prescription", - "Eyeball Frog", - "Eyedrops", - "Claim Check", - } - - def __init__(self, value: typing.Iterable[str]): - if not value: - value = self.default - super().__init__(value) +class AdultTradeStart(Choice): + """Choose the item that starts the adult trade sequence.""" + display_name = "Adult Trade Sequence Start" + option_pocket_egg = 0 + option_pocket_cucco = 1 + option_cojiro = 2 + option_odd_mushroom = 3 + option_poachers_saw = 4 + option_broken_sword = 5 + option_prescription = 6 + option_eyeball_frog = 7 + option_eyedrops = 8 + option_claim_check = 9 + default = 9 itempool_options: typing.Dict[str, type(Option)] = { @@ -1068,7 +1160,7 @@ class Targeting(Choice): class DisplayDpad(DefaultOnToggle): - """Show dpad icon on HUD for quick actions (ocarina, hover boots, iron boots).""" + """Show dpad icon on HUD for quick actions (ocarina, hover boots, iron boots, mask).""" display_name = "Display D-Pad HUD" @@ -1191,7 +1283,6 @@ oot_options: typing.Dict[str, type(Option)] = { **world_options, **bridge_options, **dungeon_items_options, - # **lacs_options, **shuffle_options, **timesavers_options, **misc_options, diff --git a/worlds/oot/Patches.py b/worlds/oot/Patches.py index ab1e75d1b9..f83b34183c 100644 --- a/worlds/oot/Patches.py +++ b/worlds/oot/Patches.py @@ -2094,10 +2094,14 @@ def patch_rom(world, rom): if not world.dungeon_mq['Ganons Castle']: chest_name = 'Ganons Castle Light Trial Lullaby Chest' location = world.get_location(chest_name) - if location.item.game == 'Ocarina of Time': - item = read_rom_item(rom, location.item.index) + if not location.item.trap: + if location.item.game == 'Ocarina of Time': + item = read_rom_item(rom, location.item.index) + else: + item = read_rom_item(rom, AP_PROGRESSION if location.item.advancement else AP_JUNK) else: - item = read_rom_item(rom, AP_PROGRESSION if location.item.advancement else AP_JUNK) + looks_like_index = get_override_entry(world, location)[5] + item = read_rom_item(rom, looks_like_index) if item['chest_type'] in (GOLD_CHEST, GILDED_CHEST, SKULL_CHEST_BIG): rom.write_int16(0x321B176, 0xFC40) # original 0xFC48 @@ -2106,10 +2110,14 @@ def patch_rom(world, rom): chest_name = 'Spirit Temple Compass Chest' chest_address = 0x2B6B07C location = world.get_location(chest_name) - if location.item.game == 'Ocarina of Time': - item = read_rom_item(rom, location.item.index) + if not location.item.trap: + if location.item.game == 'Ocarina of Time': + item = read_rom_item(rom, location.item.index) + else: + item = read_rom_item(rom, AP_PROGRESSION if location.item.advancement else AP_JUNK) else: - item = read_rom_item(rom, AP_PROGRESSION if location.item.advancement else AP_JUNK) + looks_like_index = get_override_entry(world, location)[5] + item = read_rom_item(rom, looks_like_index) if item['chest_type'] in (BROWN_CHEST, SILVER_CHEST, SKULL_CHEST_SMALL): rom.write_int16(chest_address + 2, 0x0190) # X pos rom.write_int16(chest_address + 6, 0xFABC) # Z pos @@ -2120,10 +2128,14 @@ def patch_rom(world, rom): chest_address_0 = 0x21A02D0 # Address in setup 0 chest_address_2 = 0x21A06E4 # Address in setup 2 location = world.get_location(chest_name) - if location.item.game == 'Ocarina of Time': - item = read_rom_item(rom, location.item.index) + if not location.item.trap: + if location.item.game == 'Ocarina of Time': + item = read_rom_item(rom, location.item.index) + else: + item = read_rom_item(rom, AP_PROGRESSION if location.item.advancement else AP_JUNK) else: - item = read_rom_item(rom, AP_PROGRESSION if location.item.advancement else AP_JUNK) + looks_like_index = get_override_entry(world, location)[5] + item = read_rom_item(rom, looks_like_index) if item['chest_type'] in (BROWN_CHEST, SILVER_CHEST, SKULL_CHEST_SMALL): rom.write_int16(chest_address_0 + 6, 0x0172) # Z pos rom.write_int16(chest_address_2 + 6, 0x0172) # Z pos diff --git a/worlds/oot/Rules.py b/worlds/oot/Rules.py index 1f44cebdcf..fa198e0ce1 100644 --- a/worlds/oot/Rules.py +++ b/worlds/oot/Rules.py @@ -223,9 +223,6 @@ def set_shop_rules(ootworld): # The goal is to automatically set item rules based on age requirements in case entrances were shuffled def set_entrances_based_rules(ootworld): - if ootworld.multiworld.accessibility == 'beatable': - return - all_state = ootworld.multiworld.get_all_state(False) for location in filter(lambda location: location.type == 'Shop', ootworld.get_locations()): diff --git a/worlds/oot/__init__.py b/worlds/oot/__init__.py index 539abd9674..6af19683f4 100644 --- a/worlds/oot/__init__.py +++ b/worlds/oot/__init__.py @@ -10,7 +10,7 @@ from string import printable logger = logging.getLogger("Ocarina of Time") -from .Location import OOTLocation, LocationFactory, location_name_to_id +from .Location import OOTLocation, LocationFactory, location_name_to_id, build_location_name_groups from .Entrance import OOTEntrance from .EntranceShuffle import shuffle_random_entrances, entrance_shuffle_table, EntranceShuffleError from .HintList import getRequiredHints @@ -163,11 +163,13 @@ class OOTWorld(World): "Bottle with Big Poe", "Bottle with Red Potion", "Bottle with Green Potion", "Bottle with Blue Potion", "Bottle with Fairy", "Bottle with Fish", "Bottle with Blue Fire", "Bottle with Bugs", "Bottle with Poe"}, - "Adult Trade Item": {"Pocket Egg", "Pocket Cucco", "Odd Mushroom", + "Adult Trade Item": {"Pocket Egg", "Pocket Cucco", "Cojiro", "Odd Mushroom", "Odd Potion", "Poachers Saw", "Broken Sword", "Prescription", - "Eyeball Frog", "Eyedrops", "Claim Check"} + "Eyeball Frog", "Eyedrops", "Claim Check"}, } + location_name_groups = build_location_name_groups() + def __init__(self, world, player): self.hint_data_available = threading.Event() self.collectible_flags_available = threading.Event() @@ -384,6 +386,7 @@ class OOTWorld(World): self.mq_dungeons_mode = 'count' self.mq_dungeons_count = 0 self.dungeon_mq = {item['name']: (item['name'] in mq_dungeons) for item in dungeon_table} + self.dungeon_mq['Thieves Hideout'] = False # fix for bug in SaveContext:287 # Empty dungeon placeholder for the moment self.empty_dungeons = {name: False for name in self.dungeon_mq} @@ -409,6 +412,9 @@ class OOTWorld(World): self.starting_tod = self.starting_tod.replace('_', '-') self.shuffle_scrubs = self.shuffle_scrubs.replace('_prices', '') + # Convert adult trade option to expected Set + self.adult_trade_start = {self.adult_trade_start.title().replace('_', ' ')} + # Get hint distribution self.hint_dist_user = read_json(data_path('Hints', f'{self.hint_dist}.json')) @@ -446,7 +452,7 @@ class OOTWorld(World): self.always_hints = [hint.name for hint in getRequiredHints(self)] # Determine items which are not considered advancement based on settings. They will never be excluded. - self.nonadvancement_items = {'Double Defense'} + self.nonadvancement_items = {'Double Defense', 'Deku Stick Capacity', 'Deku Nut Capacity'} if (self.damage_multiplier != 'ohko' and self.damage_multiplier != 'quadruple' and self.shuffle_scrubs == 'off' and not self.shuffle_grotto_entrances): # nayru's love may be required to prevent forced damage @@ -633,16 +639,18 @@ class OOTWorld(World): self.multiworld.itempool.remove(item) self.hinted_dungeon_reward_locations[item.name] = loc - def create_item(self, name: str): + def create_item(self, name: str, allow_arbitrary_name: bool = False): if name in item_table: return OOTItem(name, self.player, item_table[name], False, (name in self.nonadvancement_items if getattr(self, 'nonadvancement_items', None) else False)) - return OOTItem(name, self.player, ('Event', True, None, None), True, False) + if allow_arbitrary_name: + return OOTItem(name, self.player, ('Event', True, None, None), True, False) + raise Exception(f"Invalid item name: {name}") def make_event_item(self, name, location, item=None): if item is None: - item = self.create_item(name) + item = self.create_item(name, allow_arbitrary_name=True) self.multiworld.push_item(location, item, collect=False) location.locked = True location.event = True @@ -800,23 +808,25 @@ class OOTWorld(World): self.multiworld.itempool.remove(item) self.multiworld.random.shuffle(locations) fill_restrictive(self.multiworld, self.multiworld.get_all_state(False), locations, stage_items, - single_player_placement=True, lock=True) + single_player_placement=True, lock=True, allow_excluded=True) else: for dungeon_info in dungeon_table: dungeon_name = dungeon_info['name'] locations = gather_locations(self.multiworld, fill_stage, self.player, dungeon=dungeon_name) if isinstance(locations, list): dungeon_items = list(filter(lambda item: dungeon_name in item.name, stage_items)) + if not dungeon_items: + continue for item in dungeon_items: self.multiworld.itempool.remove(item) self.multiworld.random.shuffle(locations) fill_restrictive(self.multiworld, self.multiworld.get_all_state(False), locations, dungeon_items, - single_player_placement=True, lock=True) + single_player_placement=True, lock=True, allow_excluded=True) # Place songs # 5 built-in retries because this section can fail sometimes if self.shuffle_song_items != 'any': - tries = 5 + tries = 10 if self.shuffle_song_items == 'song': song_locations = list(filter(lambda location: location.type == 'Song', self.multiworld.get_unfilled_locations(player=self.player))) @@ -852,7 +862,7 @@ class OOTWorld(World): try: self.multiworld.random.shuffle(song_locations) fill_restrictive(self.multiworld, self.multiworld.get_all_state(False), song_locations[:], songs[:], - True, True) + single_player_placement=True, lock=True, allow_excluded=True) logger.debug(f"Successfully placed songs for player {self.player} after {6 - tries} attempt(s)") except FillError as e: tries -= 1 @@ -888,7 +898,8 @@ class OOTWorld(World): self.multiworld.random.shuffle(shop_locations) for item in shop_prog + shop_junk: self.multiworld.itempool.remove(item) - fill_restrictive(self.multiworld, self.multiworld.get_all_state(False), shop_locations, shop_prog, True, True) + fill_restrictive(self.multiworld, self.multiworld.get_all_state(False), shop_locations, shop_prog, + single_player_placement=True, lock=True, allow_excluded=True) fast_fill(self.multiworld, shop_junk, shop_locations) for loc in shop_locations: loc.locked = True @@ -963,7 +974,7 @@ class OOTWorld(World): multiworld.itempool.remove(item) multiworld.random.shuffle(locations) fill_restrictive(multiworld, multiworld.get_all_state(False), locations, group_stage_items, - single_player_placement=False, lock=True) + single_player_placement=False, lock=True, allow_excluded=True) if fill_stage == 'Song': # We don't want song locations to contain progression unless it's a song # or it was marked as priority. @@ -984,7 +995,7 @@ class OOTWorld(World): multiworld.itempool.remove(item) multiworld.random.shuffle(locations) fill_restrictive(multiworld, multiworld.get_all_state(False), locations, group_dungeon_items, - single_player_placement=False, lock=True) + single_player_placement=False, lock=True, allow_excluded=True) def generate_output(self, output_directory: str): if self.hints != 'none': @@ -1051,7 +1062,10 @@ class OOTWorld(World): def stage_generate_output(cls, multiworld: MultiWorld, output_directory: str): def hint_type_players(hint_type: str) -> set: return {autoworld.player for autoworld in multiworld.get_game_worlds("Ocarina of Time") - if autoworld.hints != 'none' and autoworld.hint_dist_user['distribution'][hint_type]['copies'] > 0} + if autoworld.hints != 'none' + and autoworld.hint_dist_user['distribution'][hint_type]['copies'] > 0 + and (autoworld.hint_dist_user['distribution'][hint_type]['fixed'] > 0 + or autoworld.hint_dist_user['distribution'][hint_type]['weight'] > 0)} try: item_hint_players = hint_type_players('item') @@ -1078,10 +1092,10 @@ class OOTWorld(World): if loc.game == "Ocarina of Time" and loc.item.code and (not loc.locked or (oot_is_item_of_type(loc.item, 'Song') or - (oot_is_item_of_type(loc.item, 'SmallKey') and multiworld.worlds[loc.player].shuffle_smallkeys == 'any_dungeon') or - (oot_is_item_of_type(loc.item, 'HideoutSmallKey') and multiworld.worlds[loc.player].shuffle_hideoutkeys == 'any_dungeon') or - (oot_is_item_of_type(loc.item, 'BossKey') and multiworld.worlds[loc.player].shuffle_bosskeys == 'any_dungeon') or - (oot_is_item_of_type(loc.item, 'GanonBossKey') and multiworld.worlds[loc.player].shuffle_ganon_bosskey == 'any_dungeon'))): + (oot_is_item_of_type(loc.item, 'SmallKey') and multiworld.worlds[loc.player].shuffle_smallkeys in ('overworld', 'any_dungeon', 'regional')) or + (oot_is_item_of_type(loc.item, 'HideoutSmallKey') and multiworld.worlds[loc.player].shuffle_hideoutkeys in ('overworld', 'any_dungeon', 'regional')) or + (oot_is_item_of_type(loc.item, 'BossKey') and multiworld.worlds[loc.player].shuffle_bosskeys in ('overworld', 'any_dungeon', 'regional')) or + (oot_is_item_of_type(loc.item, 'GanonBossKey') and multiworld.worlds[loc.player].shuffle_ganon_bosskey in ('overworld', 'any_dungeon', 'regional')))): if loc.player in barren_hint_players: hint_area = get_hint_area(loc) items_by_region[loc.player][hint_area]['weight'] += 1 @@ -1096,7 +1110,12 @@ class OOTWorld(World): elif barren_hint_players or woth_hint_players: # Check only relevant oot locations for barren/woth for player in (barren_hint_players | woth_hint_players): for loc in multiworld.worlds[player].get_locations(): - if loc.item.code and (not loc.locked or oot_is_item_of_type(loc.item, 'Song')): + if loc.item.code and (not loc.locked or + (oot_is_item_of_type(loc.item, 'Song') or + (oot_is_item_of_type(loc.item, 'SmallKey') and multiworld.worlds[loc.player].shuffle_smallkeys in ('overworld', 'any_dungeon', 'regional')) or + (oot_is_item_of_type(loc.item, 'HideoutSmallKey') and multiworld.worlds[loc.player].shuffle_hideoutkeys in ('overworld', 'any_dungeon', 'regional')) or + (oot_is_item_of_type(loc.item, 'BossKey') and multiworld.worlds[loc.player].shuffle_bosskeys in ('overworld', 'any_dungeon', 'regional')) or + (oot_is_item_of_type(loc.item, 'GanonBossKey') and multiworld.worlds[loc.player].shuffle_ganon_bosskey in ('overworld', 'any_dungeon', 'regional')))): if player in barren_hint_players: hint_area = get_hint_area(loc) items_by_region[player][hint_area]['weight'] += 1 @@ -1183,6 +1202,15 @@ class OOTWorld(World): er_hint_data[self.player][location.address] = main_entrance.name logger.debug(f"Set {location.name} hint data to {main_entrance.name}") + def write_spoiler(self, spoiler_handle: typing.TextIO) -> None: + required_trials_str = ", ".join(t for t in self.skipped_trials if not self.skipped_trials[t]) + spoiler_handle.write(f"\n\nTrials ({self.multiworld.get_player_name(self.player)}): {required_trials_str}\n") + + if self.shopsanity != 'off': + spoiler_handle.write(f"\nShop Prices ({self.multiworld.get_player_name(self.player)}):\n") + for k, v in self.shop_prices.items(): + spoiler_handle.write(f"{k}: {v} Rupees\n") + # Key ring handling: # Key rings are multiple items glued together into one, so we need to give # the appropriate number of keys in the collection state when they are @@ -1265,25 +1293,13 @@ class OOTWorld(World): # Specifically ensures that only real items are gotten, not any events. # In particular, ensures that Time Travel needs to be found. def get_state_with_complete_itempool(self): - all_state = self.multiworld.get_all_state(use_cache=False) - # Remove event progression items - for item, player in all_state.prog_items: - if player == self.player and (item not in item_table or item_table[item][2] is None): - all_state.prog_items[(item, player)] = 0 - # Remove all events and checked locations - all_state.locations_checked = {loc for loc in all_state.locations_checked if loc.player != self.player} - all_state.events = {loc for loc in all_state.events if loc.player != self.player} + all_state = CollectionState(self.multiworld) + for item in self.multiworld.itempool: + if item.player == self.player: + self.multiworld.worlds[item.player].collect(all_state, item) # If free_scarecrow give Scarecrow Song if self.free_scarecrow: all_state.collect(self.create_item("Scarecrow Song"), event=True) - - # Invalidate caches - all_state.child_reachable_regions[self.player] = set() - all_state.adult_reachable_regions[self.player] = set() - all_state.child_blocked_connections[self.player] = set() - all_state.adult_blocked_connections[self.player] = set() - all_state.day_reachable_regions[self.player] = set() - all_state.dampe_reachable_regions[self.player] = set() all_state.stale[self.player] = True return all_state @@ -1349,7 +1365,7 @@ def gather_locations(multiworld: MultiWorld, condition = lambda location: location.name in dungeon_song_locations locations += filter(condition, multiworld.get_unfilled_locations(player=player)) else: - if any(map(lambda v: v in {'keysanity'}, fill_opts.values())): + if any(map(lambda v: v == 'keysanity', fill_opts.values())): return None for player, option in fill_opts.items(): condition = functools.partial(valid_dungeon_item_location, From 195cf60e8ab2bc3674850044a85def0a416a111e Mon Sep 17 00:00:00 2001 From: Alchav <59858495+Alchav@users.noreply.github.com> Date: Mon, 23 Oct 2023 13:28:16 -0400 Subject: [PATCH 31/54] =?UTF-8?q?Pok=C3=A9mon=20R/B:=20Door=20Shuffle=20ef?= =?UTF-8?q?ficiency=20improvement=20and=20crash=20fix=20(#2347)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Sweep only current player's locations so that more players does not slow it down. Fix a slight possibility of Full door shuffle crash by only sorting for outdoor dead ends only when connecting from a non-dead end. --- worlds/pokemon_rb/regions.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/worlds/pokemon_rb/regions.py b/worlds/pokemon_rb/regions.py index 431b23f49a..1816d010c0 100644 --- a/worlds/pokemon_rb/regions.py +++ b/worlds/pokemon_rb/regions.py @@ -2267,9 +2267,12 @@ def create_regions(self): "Defeat Viridian Gym Giovanni", ] + event_locations = self.multiworld.get_filled_locations(player) + def adds_reachable_entrances(entrances_copy, item): state_copy = state.copy() - state_copy.collect(item, False) + state_copy.collect(item, True) + state.sweep_for_events(locations=event_locations) ret = len([entrance for entrance in entrances_copy if entrance in reachable_entrances or entrance.parent_region.can_reach(state_copy)]) > len(reachable_entrances) return ret @@ -2305,7 +2308,6 @@ def create_regions(self): starting_entrances = len(entrances) dc_connected = [] - event_locations = self.multiworld.get_filled_locations(player) rock_tunnel_entrances = [entrance for entrance in entrances if "Rock Tunnel" in entrance.name] entrances = [entrance for entrance in entrances if entrance not in rock_tunnel_entrances] while entrances: @@ -2330,10 +2332,6 @@ def create_regions(self): if multiworld.door_shuffle[player] == "full" or len(entrances) != len(reachable_entrances): entrances.sort(key=lambda e: e.name not in entrance_only) - if len(entrances) < 48 and multiworld.door_shuffle[player] == "full": - # Prevent a situation where the only remaining outdoor entrances are ones that cannot be reached - # except by connecting directly to it. - entrances.sort(key=lambda e: e.name in unreachable_outdoor_entrances) # entrances list is empty while it's being sorted, must pass a copy to iterate through entrances_copy = entrances.copy() if multiworld.door_shuffle[player] == "decoupled": @@ -2350,6 +2348,11 @@ def create_regions(self): dead_end(entrances_copy, e) else 2) if multiworld.door_shuffle[player] == "full": outdoor = outdoor_map(entrances[0].parent_region.name) + if len(entrances) < 48 and not outdoor: + # Prevent a situation where the only remaining outdoor entrances are ones that cannot be reached + # except by connecting directly to it. + entrances.sort(key=lambda e: e.name in unreachable_outdoor_entrances) + entrances.sort(key=lambda e: outdoor_map(e.parent_region.name) != outdoor) assert entrances[0] in reachable_entrances, \ "Ran out of valid reachable entrances in Pokemon Red and Blue door shuffle" From e394c316f5760e8e70bc642b6dbbd693fdf9e5e2 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Mon, 23 Oct 2023 22:07:24 +0200 Subject: [PATCH 32/54] Setup: new setup experience (read: torch almost all of it) (#2268) --- inno_setup.iss | 871 +++++-------------------------------------------- 1 file changed, 79 insertions(+), 792 deletions(-) diff --git a/inno_setup.iss b/inno_setup.iss index 3c1bdc4571..b6f40f7701 100644 --- a/inno_setup.iss +++ b/inno_setup.iss @@ -46,151 +46,33 @@ Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{ [Types] Name: "full"; Description: "Full installation" -Name: "hosting"; Description: "Installation for hosting purposes" -Name: "playing"; Description: "Installation for playing purposes" +Name: "minimal"; Description: "Minimal installation" Name: "custom"; Description: "Custom installation"; Flags: iscustom [Components] -Name: "core"; Description: "Core Files"; Types: full hosting playing custom; Flags: fixed -Name: "generator"; Description: "Generator"; Types: full hosting -Name: "generator/sm"; Description: "Super Metroid ROM Setup"; Types: full hosting; ExtraDiskSpaceRequired: 3145728; Flags: disablenouninstallwarning -Name: "generator/dkc3"; Description: "Donkey Kong Country 3 ROM Setup"; Types: full hosting; ExtraDiskSpaceRequired: 3145728; Flags: disablenouninstallwarning -Name: "generator/smw"; Description: "Super Mario World ROM Setup"; Types: full hosting; ExtraDiskSpaceRequired: 3145728; Flags: disablenouninstallwarning -Name: "generator/soe"; Description: "Secret of Evermore ROM Setup"; Types: full hosting; ExtraDiskSpaceRequired: 3145728; Flags: disablenouninstallwarning -Name: "generator/l2ac"; Description: "Lufia II Ancient Cave ROM Setup"; Types: full hosting; ExtraDiskSpaceRequired: 2621440; Flags: disablenouninstallwarning -Name: "generator/lttp"; Description: "A Link to the Past ROM Setup and Enemizer"; Types: full hosting; ExtraDiskSpaceRequired: 5191680 -Name: "generator/oot"; Description: "Ocarina of Time ROM Setup"; Types: full hosting; ExtraDiskSpaceRequired: 100663296; Flags: disablenouninstallwarning -Name: "generator/zl"; Description: "Zillion ROM Setup"; Types: full hosting; ExtraDiskSpaceRequired: 150000; Flags: disablenouninstallwarning -Name: "generator/pkmn_r"; Description: "Pokemon Red ROM Setup"; Types: full hosting -Name: "generator/pkmn_b"; Description: "Pokemon Blue ROM Setup"; Types: full hosting -Name: "generator/mmbn3"; Description: "MegaMan Battle Network 3"; Types: full hosting; ExtraDiskSpaceRequired: 8388608; Flags: disablenouninstallwarning -Name: "generator/ladx"; Description: "Link's Awakening DX ROM Setup"; Types: full hosting -Name: "generator/tloz"; Description: "The Legend of Zelda ROM Setup"; Types: full hosting; ExtraDiskSpaceRequired: 135168; Flags: disablenouninstallwarning -Name: "server"; Description: "Server"; Types: full hosting -Name: "client"; Description: "Clients"; Types: full playing -Name: "client/sni"; Description: "SNI Client"; Types: full playing -Name: "client/sni/lttp"; Description: "SNI Client - A Link to the Past Patch Setup"; Types: full playing; Flags: disablenouninstallwarning -Name: "client/sni/sm"; Description: "SNI Client - Super Metroid Patch Setup"; Types: full playing; Flags: disablenouninstallwarning -Name: "client/sni/dkc3"; Description: "SNI Client - Donkey Kong Country 3 Patch Setup"; Types: full playing; Flags: disablenouninstallwarning -Name: "client/sni/smw"; Description: "SNI Client - Super Mario World Patch Setup"; Types: full playing; Flags: disablenouninstallwarning -Name: "client/sni/l2ac"; Description: "SNI Client - Lufia II Ancient Cave Patch Setup"; Types: full playing; Flags: disablenouninstallwarning -Name: "client/bizhawk"; Description: "BizHawk Client"; Types: full playing -Name: "client/factorio"; Description: "Factorio"; Types: full playing -Name: "client/kh2"; Description: "Kingdom Hearts 2"; Types: full playing -Name: "client/minecraft"; Description: "Minecraft"; Types: full playing; ExtraDiskSpaceRequired: 226894278 -Name: "client/oot"; Description: "Ocarina of Time"; Types: full playing -Name: "client/ff1"; Description: "Final Fantasy 1"; Types: full playing -Name: "client/pkmn"; Description: "Pokemon Client" -Name: "client/pkmn/red"; Description: "Pokemon Client - Pokemon Red Setup"; Types: full playing; ExtraDiskSpaceRequired: 1048576 -Name: "client/pkmn/blue"; Description: "Pokemon Client - Pokemon Blue Setup"; Types: full playing; ExtraDiskSpaceRequired: 1048576 -Name: "client/mmbn3"; Description: "MegaMan Battle Network 3 Client"; Types: full playing; -Name: "client/ladx"; Description: "Link's Awakening Client"; Types: full playing; ExtraDiskSpaceRequired: 1048576 -Name: "client/cf"; Description: "ChecksFinder"; Types: full playing -Name: "client/sc2"; Description: "Starcraft 2"; Types: full playing -Name: "client/wargroove"; Description: "Wargroove"; Types: full playing -Name: "client/zl"; Description: "Zillion"; Types: full playing -Name: "client/tloz"; Description: "The Legend of Zelda"; Types: full playing -Name: "client/advn"; Description: "Adventure"; Types: full playing -Name: "client/ut"; Description: "Undertale"; Types: full playing -Name: "client/text"; Description: "Text, to !command and chat"; Types: full playing +Name: "core"; Description: "Archipelago"; Types: full minimal custom; Flags: fixed +Name: "lttp_sprites"; Description: "Download ""A Link to the Past"" player sprites"; Types: full; [Dirs] NAME: "{app}"; Flags: setntfscompression; Permissions: everyone-modify users-modify authusers-modify; [Files] -Source: "{code:GetROMPath}"; DestDir: "{app}"; DestName: "Zelda no Densetsu - Kamigami no Triforce (Japan).sfc"; Flags: external; Components: client/sni/lttp or generator/lttp -Source: "{code:GetSMROMPath}"; DestDir: "{app}"; DestName: "Super Metroid (JU).sfc"; Flags: external; Components: client/sni/sm or generator/sm -Source: "{code:GetDKC3ROMPath}"; DestDir: "{app}"; DestName: "Donkey Kong Country 3 - Dixie Kong's Double Trouble! (USA) (En,Fr).sfc"; Flags: external; Components: client/sni/dkc3 or generator/dkc3 -Source: "{code:GetSMWROMPath}"; DestDir: "{app}"; DestName: "Super Mario World (USA).sfc"; Flags: external; Components: client/sni/smw or generator/smw -Source: "{code:GetSoEROMPath}"; DestDir: "{app}"; DestName: "Secret of Evermore (USA).sfc"; Flags: external; Components: generator/soe -Source: "{code:GetL2ACROMPath}"; DestDir: "{app}"; DestName: "Lufia II - Rise of the Sinistrals (USA).sfc"; Flags: external; Components: generator/l2ac -Source: "{code:GetOoTROMPath}"; DestDir: "{app}"; DestName: "The Legend of Zelda - Ocarina of Time.z64"; Flags: external; Components: client/oot or generator/oot -Source: "{code:GetZlROMPath}"; DestDir: "{app}"; DestName: "Zillion (UE) [!].sms"; Flags: external; Components: client/zl or generator/zl -Source: "{code:GetRedROMPath}"; DestDir: "{app}"; DestName: "Pokemon Red (UE) [S][!].gb"; Flags: external; Components: client/pkmn/red or generator/pkmn_r -Source: "{code:GetBlueROMPath}"; DestDir: "{app}"; DestName: "Pokemon Blue (UE) [S][!].gb"; Flags: external; Components: client/pkmn/blue or generator/pkmn_b -Source: "{code:GetBN3ROMPath}"; DestDir: "{app}"; DestName: "Mega Man Battle Network 3 - Blue Version (USA).gba"; Flags: external; Components: client/mmbn3 -Source: "{code:GetLADXROMPath}"; DestDir: "{app}"; DestName: "Legend of Zelda, The - Link's Awakening DX (USA, Europe) (SGB Enhanced).gbc"; Flags: external; Components: client/ladx or generator/ladx -Source: "{code:GetTLoZROMPath}"; DestDir: "{app}"; DestName: "Legend of Zelda, The (U) (PRG0) [!].nes"; Flags: external; Components: client/tloz or generator/tloz -Source: "{code:GetAdvnROMPath}"; DestDir: "{app}"; DestName: "ADVNTURE.BIN"; Flags: external; Components: client/advn -Source: "{#source_path}\*"; Excludes: "*.sfc, *.log, data\sprites\alttpr, SNI, EnemizerCLI, Archipelago*.exe"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs -Source: "{#source_path}\SNI\*"; Excludes: "*.sfc, *.log"; DestDir: "{app}\SNI"; Flags: ignoreversion recursesubdirs createallsubdirs; Components: client/sni -Source: "{#source_path}\EnemizerCLI\*"; Excludes: "*.sfc, *.log"; DestDir: "{app}\EnemizerCLI"; Flags: ignoreversion recursesubdirs createallsubdirs; Components: generator/lttp - -Source: "{#source_path}\ArchipelagoLauncher.exe"; DestDir: "{app}"; Flags: ignoreversion; -Source: "{#source_path}\ArchipelagoLauncher(DEBUG).exe"; DestDir: "{app}"; Flags: ignoreversion; -Source: "{#source_path}\ArchipelagoGenerate.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: generator -Source: "{#source_path}\ArchipelagoServer.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: server -Source: "{#source_path}\ArchipelagoFactorioClient.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/factorio -Source: "{#source_path}\ArchipelagoTextClient.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/text -Source: "{#source_path}\ArchipelagoSNIClient.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/sni -Source: "{#source_path}\ArchipelagoBizHawkClient.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/bizhawk -Source: "{#source_path}\ArchipelagoLinksAwakeningClient.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/ladx -Source: "{#source_path}\ArchipelagoLttPAdjuster.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/sni/lttp or generator/lttp -Source: "{#source_path}\ArchipelagoMinecraftClient.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/minecraft -Source: "{#source_path}\ArchipelagoOoTClient.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/oot -Source: "{#source_path}\ArchipelagoOoTAdjuster.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/oot -Source: "{#source_path}\ArchipelagoZillionClient.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/zl -Source: "{#source_path}\ArchipelagoFF1Client.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/ff1 -Source: "{#source_path}\ArchipelagoPokemonClient.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/pkmn -Source: "{#source_path}\ArchipelagoChecksFinderClient.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/cf -Source: "{#source_path}\ArchipelagoStarcraft2Client.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/sc2 -Source: "{#source_path}\ArchipelagoMMBN3Client.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/mmbn3 -Source: "{#source_path}\ArchipelagoZelda1Client.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/tloz -Source: "{#source_path}\ArchipelagoWargrooveClient.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/wargroove -Source: "{#source_path}\ArchipelagoKH2Client.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/kh2 -Source: "{#source_path}\ArchipelagoAdventureClient.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/advn -Source: "{#source_path}\ArchipelagoUndertaleClient.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/ut +Source: "{#source_path}\*"; Excludes: "*.sfc, *.log, data\sprites\alttpr, SNI, EnemizerCLI"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs +Source: "{#source_path}\SNI\*"; Excludes: "*.sfc, *.log"; DestDir: "{app}\SNI"; Flags: ignoreversion recursesubdirs createallsubdirs; +Source: "{#source_path}\EnemizerCLI\*"; Excludes: "*.sfc, *.log"; DestDir: "{app}\EnemizerCLI"; Flags: ignoreversion recursesubdirs createallsubdirs; Source: "vc_redist.x64.exe"; DestDir: {tmp}; Flags: deleteafterinstall [Icons] Name: "{group}\{#MyAppName} Folder"; Filename: "{app}"; Name: "{group}\{#MyAppName} Launcher"; Filename: "{app}\ArchipelagoLauncher.exe" -Name: "{group}\{#MyAppName} Server"; Filename: "{app}\ArchipelagoServer"; Components: server -Name: "{group}\{#MyAppName} Text Client"; Filename: "{app}\ArchipelagoTextClient.exe"; Components: client/text -Name: "{group}\{#MyAppName} SNI Client"; Filename: "{app}\ArchipelagoSNIClient.exe"; Components: client/sni -Name: "{group}\{#MyAppName} BizHawk Client"; Filename: "{app}\ArchipelagoBizHawkClient.exe"; Components: client/bizhawk -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} Ocarina of Time Client"; Filename: "{app}\ArchipelagoOoTClient.exe"; Components: client/oot -Name: "{group}\{#MyAppName} Zillion Client"; Filename: "{app}\ArchipelagoZillionClient.exe"; Components: client/zl -Name: "{group}\{#MyAppName} Final Fantasy 1 Client"; Filename: "{app}\ArchipelagoFF1Client.exe"; Components: client/ff1 -Name: "{group}\{#MyAppName} Pokemon Client"; Filename: "{app}\ArchipelagoPokemonClient.exe"; Components: client/pkmn -Name: "{group}\{#MyAppName} ChecksFinder Client"; Filename: "{app}\ArchipelagoChecksFinderClient.exe"; Components: client/cf -Name: "{group}\{#MyAppName} Starcraft 2 Client"; Filename: "{app}\ArchipelagoStarcraft2Client.exe"; Components: client/sc2 -Name: "{group}\{#MyAppName} MegaMan Battle Network 3 Client"; Filename: "{app}\ArchipelagoMMBN3Client.exe"; Components: client/mmbn3 -Name: "{group}\{#MyAppName} The Legend of Zelda Client"; Filename: "{app}\ArchipelagoZelda1Client.exe"; Components: client/tloz -Name: "{group}\{#MyAppName} Kingdom Hearts 2 Client"; Filename: "{app}\ArchipelagoKH2Client.exe"; Components: client/kh2 -Name: "{group}\{#MyAppName} Link's Awakening Client"; Filename: "{app}\ArchipelagoLinksAwakeningClient.exe"; Components: client/ladx -Name: "{group}\{#MyAppName} Adventure Client"; Filename: "{app}\ArchipelagoAdventureClient.exe"; Components: client/advn -Name: "{group}\{#MyAppName} Wargroove Client"; Filename: "{app}\ArchipelagoWargrooveClient.exe"; Components: client/wargroove -Name: "{group}\{#MyAppName} Undertale Client"; Filename: "{app}\ArchipelagoUndertaleClient.exe"; Components: client/ut Name: "{commondesktop}\{#MyAppName} Folder"; Filename: "{app}"; Tasks: desktopicon Name: "{commondesktop}\{#MyAppName} Launcher"; Filename: "{app}\ArchipelagoLauncher.exe"; Tasks: desktopicon -Name: "{commondesktop}\{#MyAppName} Server"; Filename: "{app}\ArchipelagoServer"; Tasks: desktopicon; Components: server -Name: "{commondesktop}\{#MyAppName} SNI Client"; Filename: "{app}\ArchipelagoSNIClient.exe"; Tasks: desktopicon; Components: client/sni -Name: "{commondesktop}\{#MyAppName} BizHawk Client"; Filename: "{app}\ArchipelagoBizHawkClient.exe"; Tasks: desktopicon; Components: client/bizhawk -Name: "{commondesktop}\{#MyAppName} Factorio Client"; Filename: "{app}\ArchipelagoFactorioClient.exe"; Tasks: desktopicon; Components: client/factorio -Name: "{commondesktop}\{#MyAppName} Minecraft Client"; Filename: "{app}\ArchipelagoMinecraftClient.exe"; Tasks: desktopicon; Components: client/minecraft -Name: "{commondesktop}\{#MyAppName} Ocarina of Time Client"; Filename: "{app}\ArchipelagoOoTClient.exe"; Tasks: desktopicon; Components: client/oot -Name: "{commondesktop}\{#MyAppName} Zillion Client"; Filename: "{app}\ArchipelagoZillionClient.exe"; Tasks: desktopicon; Components: client/zl -Name: "{commondesktop}\{#MyAppName} Final Fantasy 1 Client"; Filename: "{app}\ArchipelagoFF1Client.exe"; Tasks: desktopicon; Components: client/ff1 -Name: "{commondesktop}\{#MyAppName} Pokemon Client"; Filename: "{app}\ArchipelagoPokemonClient.exe"; Tasks: desktopicon; Components: client/pkmn -Name: "{commondesktop}\{#MyAppName} ChecksFinder Client"; Filename: "{app}\ArchipelagoChecksFinderClient.exe"; Tasks: desktopicon; Components: client/cf -Name: "{commondesktop}\{#MyAppName} Starcraft 2 Client"; Filename: "{app}\ArchipelagoStarcraft2Client.exe"; Tasks: desktopicon; Components: client/sc2 -Name: "{commondesktop}\{#MyAppName} MegaMan Battle Network 3 Client"; Filename: "{app}\ArchipelagoMMBN3Client.exe"; Tasks: desktopicon; Components: client/mmbn3 -Name: "{commondesktop}\{#MyAppName} The Legend of Zelda Client"; Filename: "{app}\ArchipelagoZelda1Client.exe"; Tasks: desktopicon; Components: client/tloz -Name: "{commondesktop}\{#MyAppName} Wargroove Client"; Filename: "{app}\ArchipelagoWargrooveClient.exe"; Tasks: desktopicon; Components: client/wargroove -Name: "{commondesktop}\{#MyAppName} Kingdom Hearts 2 Client"; Filename: "{app}\ArchipelagoKH2Client.exe"; Tasks: desktopicon; Components: client/kh2 -Name: "{commondesktop}\{#MyAppName} Link's Awakening Client"; Filename: "{app}\ArchipelagoLinksAwakeningClient.exe"; Tasks: desktopicon; Components: client/ladx -Name: "{commondesktop}\{#MyAppName} Adventure Client"; Filename: "{app}\ArchipelagoAdventureClient.exe"; Tasks: desktopicon; Components: client/advn -Name: "{commondesktop}\{#MyAppName} Undertale Client"; Filename: "{app}\ArchipelagoUndertaleClient.exe"; Tasks: desktopicon; Components: client/ut [Run] Filename: "{tmp}\vc_redist.x64.exe"; Parameters: "/passive /norestart"; Check: IsVCRedist64BitNeeded; StatusMsg: "Installing VC++ redistributable..." -Filename: "{app}\ArchipelagoLttPAdjuster"; Parameters: "--update_sprites"; StatusMsg: "Updating Sprite Library..."; Components: client/sni/lttp or generator/lttp -Filename: "{app}\ArchipelagoMinecraftClient.exe"; Parameters: "--install"; StatusMsg: "Installing Forge Server..."; Components: client/minecraft +Filename: "{app}\ArchipelagoLttPAdjuster"; Parameters: "--update_sprites"; StatusMsg: "Updating Sprite Library..."; Flags: nowait; Components: lttp_sprites Filename: "{app}\ArchipelagoLauncher"; Parameters: "--update_settings"; StatusMsg: "Updating host.yaml..."; Flags: runasoriginaluser runhidden Filename: "{app}\ArchipelagoLauncher"; Description: "{cm:LaunchProgram,{#StringChange('Launcher', '&', '&&')}}"; Flags: nowait postinstall skipifsilent @@ -206,101 +88,97 @@ Type: filesandordirs; Name: "{app}\EnemizerCLI*" [Registry] -Root: HKCR; Subkey: ".aplttp"; ValueData: "{#MyAppName}patch"; Flags: uninsdeletevalue; ValueType: string; ValueName: ""; Components: client/sni -Root: HKCR; Subkey: "{#MyAppName}patch"; ValueData: "Archipelago Binary Patch"; Flags: uninsdeletekey; ValueType: string; ValueName: ""; Components: client/sni -Root: HKCR; Subkey: "{#MyAppName}patch\DefaultIcon"; ValueData: "{app}\ArchipelagoSNIClient.exe,0"; ValueType: string; ValueName: ""; Components: client/sni -Root: HKCR; Subkey: "{#MyAppName}patch\shell\open\command"; ValueData: """{app}\ArchipelagoSNIClient.exe"" ""%1"""; ValueType: string; ValueName: ""; Components: client/sni +Root: HKCR; Subkey: ".aplttp"; ValueData: "{#MyAppName}patch"; Flags: uninsdeletevalue; ValueType: string; ValueName: ""; +Root: HKCR; Subkey: "{#MyAppName}patch"; ValueData: "Archipelago Binary Patch"; Flags: uninsdeletekey; ValueType: string; ValueName: ""; +Root: HKCR; Subkey: "{#MyAppName}patch\DefaultIcon"; ValueData: "{app}\ArchipelagoSNIClient.exe,0"; ValueType: string; ValueName: ""; +Root: HKCR; Subkey: "{#MyAppName}patch\shell\open\command"; ValueData: """{app}\ArchipelagoSNIClient.exe"" ""%1"""; ValueType: string; ValueName: ""; -Root: HKCR; Subkey: ".apsm"; ValueData: "{#MyAppName}smpatch"; Flags: uninsdeletevalue; ValueType: string; ValueName: ""; Components: client/sni -Root: HKCR; Subkey: "{#MyAppName}smpatch"; ValueData: "Archipelago Super Metroid Patch"; Flags: uninsdeletekey; ValueType: string; ValueName: ""; Components: client/sni -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: ".apsm"; ValueData: "{#MyAppName}smpatch"; Flags: uninsdeletevalue; ValueType: string; ValueName: ""; +Root: HKCR; Subkey: "{#MyAppName}smpatch"; ValueData: "Archipelago Super Metroid Patch"; Flags: uninsdeletekey; ValueType: string; ValueName: ""; +Root: HKCR; Subkey: "{#MyAppName}smpatch\DefaultIcon"; ValueData: "{app}\ArchipelagoSNIClient.exe,0"; ValueType: string; ValueName: ""; +Root: HKCR; Subkey: "{#MyAppName}smpatch\shell\open\command"; ValueData: """{app}\ArchipelagoSNIClient.exe"" ""%1"""; ValueType: string; ValueName: ""; -Root: HKCR; Subkey: ".apdkc3"; ValueData: "{#MyAppName}dkc3patch"; Flags: uninsdeletevalue; ValueType: string; ValueName: ""; Components: client/sni -Root: HKCR; Subkey: "{#MyAppName}dkc3patch"; ValueData: "Archipelago Donkey Kong Country 3 Patch"; Flags: uninsdeletekey; ValueType: string; ValueName: ""; Components: client/sni -Root: HKCR; Subkey: "{#MyAppName}dkc3patch\DefaultIcon"; ValueData: "{app}\ArchipelagoSNIClient.exe,0"; ValueType: string; ValueName: ""; Components: client/sni -Root: HKCR; Subkey: "{#MyAppName}dkc3patch\shell\open\command"; ValueData: """{app}\ArchipelagoSNIClient.exe"" ""%1"""; ValueType: string; ValueName: ""; Components: client/sni +Root: HKCR; Subkey: ".apdkc3"; ValueData: "{#MyAppName}dkc3patch"; Flags: uninsdeletevalue; ValueType: string; ValueName: ""; +Root: HKCR; Subkey: "{#MyAppName}dkc3patch"; ValueData: "Archipelago Donkey Kong Country 3 Patch"; Flags: uninsdeletekey; ValueType: string; ValueName: ""; +Root: HKCR; Subkey: "{#MyAppName}dkc3patch\DefaultIcon"; ValueData: "{app}\ArchipelagoSNIClient.exe,0"; ValueType: string; ValueName: ""; +Root: HKCR; Subkey: "{#MyAppName}dkc3patch\shell\open\command"; ValueData: """{app}\ArchipelagoSNIClient.exe"" ""%1"""; ValueType: string; ValueName: ""; -Root: HKCR; Subkey: ".apsmw"; ValueData: "{#MyAppName}smwpatch"; Flags: uninsdeletevalue; ValueType: string; ValueName: ""; Components: client/sni -Root: HKCR; Subkey: "{#MyAppName}smwpatch"; ValueData: "Archipelago Super Mario World Patch"; Flags: uninsdeletekey; ValueType: string; ValueName: ""; Components: client/sni -Root: HKCR; Subkey: "{#MyAppName}smwpatch\DefaultIcon"; ValueData: "{app}\ArchipelagoSNIClient.exe,0"; ValueType: string; ValueName: ""; Components: client/sni -Root: HKCR; Subkey: "{#MyAppName}smwpatch\shell\open\command"; ValueData: """{app}\ArchipelagoSNIClient.exe"" ""%1"""; ValueType: string; ValueName: ""; Components: client/sni +Root: HKCR; Subkey: ".apsmw"; ValueData: "{#MyAppName}smwpatch"; Flags: uninsdeletevalue; ValueType: string; ValueName: ""; +Root: HKCR; Subkey: "{#MyAppName}smwpatch"; ValueData: "Archipelago Super Mario World Patch"; Flags: uninsdeletekey; ValueType: string; ValueName: ""; +Root: HKCR; Subkey: "{#MyAppName}smwpatch\DefaultIcon"; ValueData: "{app}\ArchipelagoSNIClient.exe,0"; ValueType: string; ValueName: ""; +Root: HKCR; Subkey: "{#MyAppName}smwpatch\shell\open\command"; ValueData: """{app}\ArchipelagoSNIClient.exe"" ""%1"""; ValueType: string; ValueName: ""; -Root: HKCR; Subkey: ".apzl"; ValueData: "{#MyAppName}zlpatch"; Flags: uninsdeletevalue; ValueType: string; ValueName: ""; Components: client/zl -Root: HKCR; Subkey: "{#MyAppName}zlpatch"; ValueData: "Archipelago Zillion Patch"; Flags: uninsdeletekey; ValueType: string; ValueName: ""; Components: client/zl -Root: HKCR; Subkey: "{#MyAppName}zlpatch\DefaultIcon"; ValueData: "{app}\ArchipelagoZillionClient.exe,0"; ValueType: string; ValueName: ""; Components: client/zl -Root: HKCR; Subkey: "{#MyAppName}zlpatch\shell\open\command"; ValueData: """{app}\ArchipelagoZillionClient.exe"" ""%1"""; ValueType: string; ValueName: ""; Components: client/zl +Root: HKCR; Subkey: ".apzl"; ValueData: "{#MyAppName}zlpatch"; Flags: uninsdeletevalue; ValueType: string; ValueName: ""; +Root: HKCR; Subkey: "{#MyAppName}zlpatch"; ValueData: "Archipelago Zillion Patch"; Flags: uninsdeletekey; ValueType: string; ValueName: ""; +Root: HKCR; Subkey: "{#MyAppName}zlpatch\DefaultIcon"; ValueData: "{app}\ArchipelagoZillionClient.exe,0"; ValueType: string; ValueName: ""; +Root: HKCR; Subkey: "{#MyAppName}zlpatch\shell\open\command"; ValueData: """{app}\ArchipelagoZillionClient.exe"" ""%1"""; ValueType: string; ValueName: ""; -Root: HKCR; Subkey: ".apsmz3"; ValueData: "{#MyAppName}smz3patch"; Flags: uninsdeletevalue; ValueType: string; ValueName: ""; Components: client/sni -Root: HKCR; Subkey: "{#MyAppName}smz3patch"; ValueData: "Archipelago SMZ3 Patch"; Flags: uninsdeletekey; ValueType: string; ValueName: ""; Components: client/sni -Root: HKCR; Subkey: "{#MyAppName}smz3patch\DefaultIcon"; ValueData: "{app}\ArchipelagoSNIClient.exe,0"; ValueType: string; ValueName: ""; Components: client/sni -Root: HKCR; Subkey: "{#MyAppName}smz3patch\shell\open\command"; ValueData: """{app}\ArchipelagoSNIClient.exe"" ""%1"""; ValueType: string; ValueName: ""; Components: client/sni +Root: HKCR; Subkey: ".apsmz3"; ValueData: "{#MyAppName}smz3patch"; Flags: uninsdeletevalue; ValueType: string; ValueName: ""; +Root: HKCR; Subkey: "{#MyAppName}smz3patch"; ValueData: "Archipelago SMZ3 Patch"; Flags: uninsdeletekey; ValueType: string; ValueName: ""; +Root: HKCR; Subkey: "{#MyAppName}smz3patch\DefaultIcon"; ValueData: "{app}\ArchipelagoSNIClient.exe,0"; ValueType: string; ValueName: ""; +Root: HKCR; Subkey: "{#MyAppName}smz3patch\shell\open\command"; ValueData: """{app}\ArchipelagoSNIClient.exe"" ""%1"""; ValueType: string; ValueName: ""; -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"""; ValueType: string; ValueName: ""; Components: client/sni +Root: HKCR; Subkey: ".apsoe"; ValueData: "{#MyAppName}soepatch"; Flags: uninsdeletevalue; ValueType: string; ValueName: ""; +Root: HKCR; Subkey: "{#MyAppName}soepatch"; ValueData: "Archipelago Secret of Evermore Patch"; Flags: uninsdeletekey; ValueType: string; ValueName: ""; +Root: HKCR; Subkey: "{#MyAppName}soepatch\DefaultIcon"; ValueData: "{app}\ArchipelagoSNIClient.exe,0"; ValueType: string; ValueName: ""; +Root: HKCR; Subkey: "{#MyAppName}soepatch\shell\open\command"; ValueData: """{app}\ArchipelagoSNIClient.exe"" ""%1"""; ValueType: string; ValueName: ""; -Root: HKCR; Subkey: ".apl2ac"; ValueData: "{#MyAppName}l2acpatch"; Flags: uninsdeletevalue; ValueType: string; ValueName: ""; Components: client/sni -Root: HKCR; Subkey: "{#MyAppName}l2acpatch"; ValueData: "Archipelago Lufia II Ancient Cave Patch"; Flags: uninsdeletekey; ValueType: string; ValueName: ""; Components: client/sni -Root: HKCR; Subkey: "{#MyAppName}l2acpatch\DefaultIcon"; ValueData: "{app}\ArchipelagoSNIClient.exe,0"; ValueType: string; ValueName: ""; Components: client/sni -Root: HKCR; Subkey: "{#MyAppName}l2acpatch\shell\open\command"; ValueData: """{app}\ArchipelagoSNIClient.exe"" ""%1"""; ValueType: string; ValueName: ""; Components: client/sni +Root: HKCR; Subkey: ".apl2ac"; ValueData: "{#MyAppName}l2acpatch"; Flags: uninsdeletevalue; ValueType: string; ValueName: ""; +Root: HKCR; Subkey: "{#MyAppName}l2acpatch"; ValueData: "Archipelago Lufia II Ancient Cave Patch"; Flags: uninsdeletekey; ValueType: string; ValueName: ""; +Root: HKCR; Subkey: "{#MyAppName}l2acpatch\DefaultIcon"; ValueData: "{app}\ArchipelagoSNIClient.exe,0"; ValueType: string; ValueName: ""; +Root: HKCR; Subkey: "{#MyAppName}l2acpatch\shell\open\command"; ValueData: """{app}\ArchipelagoSNIClient.exe"" ""%1"""; ValueType: string; ValueName: ""; -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 -Root: HKCR; Subkey: "{#MyAppName}mcdata\shell\open\command"; ValueData: """{app}\ArchipelagoMinecraftClient.exe"" ""%1"""; ValueType: string; ValueName: ""; Components: client/minecraft +Root: HKCR; Subkey: ".apmc"; ValueData: "{#MyAppName}mcdata"; Flags: uninsdeletevalue; ValueType: string; ValueName: ""; +Root: HKCR; Subkey: "{#MyAppName}mcdata"; ValueData: "Archipelago Minecraft Data"; Flags: uninsdeletekey; ValueType: string; ValueName: ""; +Root: HKCR; Subkey: "{#MyAppName}mcdata\DefaultIcon"; ValueData: "{app}\ArchipelagoMinecraftClient.exe,0"; ValueType: string; ValueName: ""; +Root: HKCR; Subkey: "{#MyAppName}mcdata\shell\open\command"; ValueData: """{app}\ArchipelagoMinecraftClient.exe"" ""%1"""; ValueType: string; ValueName: ""; -Root: HKCR; Subkey: ".apz5"; ValueData: "{#MyAppName}n64zpf"; Flags: uninsdeletevalue; ValueType: string; ValueName: ""; Components: client/oot -Root: HKCR; Subkey: "{#MyAppName}n64zpf"; ValueData: "Archipelago Ocarina of Time Patch"; Flags: uninsdeletekey; ValueType: string; ValueName: ""; Components: client/oot -Root: HKCR; Subkey: "{#MyAppName}n64zpf\DefaultIcon"; ValueData: "{app}\ArchipelagoOoTClient.exe,0"; ValueType: string; ValueName: ""; Components: client/oot -Root: HKCR; Subkey: "{#MyAppName}n64zpf\shell\open\command"; ValueData: """{app}\ArchipelagoOoTClient.exe"" ""%1"""; ValueType: string; ValueName: ""; Components: client/oot +Root: HKCR; Subkey: ".apz5"; ValueData: "{#MyAppName}n64zpf"; Flags: uninsdeletevalue; ValueType: string; ValueName: ""; +Root: HKCR; Subkey: "{#MyAppName}n64zpf"; ValueData: "Archipelago Ocarina of Time Patch"; Flags: uninsdeletekey; ValueType: string; ValueName: ""; +Root: HKCR; Subkey: "{#MyAppName}n64zpf\DefaultIcon"; ValueData: "{app}\ArchipelagoOoTClient.exe,0"; ValueType: string; ValueName: ""; +Root: HKCR; Subkey: "{#MyAppName}n64zpf\shell\open\command"; ValueData: """{app}\ArchipelagoOoTClient.exe"" ""%1"""; ValueType: string; ValueName: ""; -Root: HKCR; Subkey: ".apred"; ValueData: "{#MyAppName}pkmnrpatch"; Flags: uninsdeletevalue; ValueType: string; ValueName: ""; Components: client/pkmn -Root: HKCR; Subkey: "{#MyAppName}pkmnrpatch"; ValueData: "Archipelago Pokemon Red Patch"; Flags: uninsdeletekey; ValueType: string; ValueName: ""; Components: client/pkmn -Root: HKCR; Subkey: "{#MyAppName}pkmnrpatch\DefaultIcon"; ValueData: "{app}\ArchipelagoPokemonClient.exe,0"; ValueType: string; ValueName: ""; Components: client/pkmn -Root: HKCR; Subkey: "{#MyAppName}pkmnrpatch\shell\open\command"; ValueData: """{app}\ArchipelagoPokemonClient.exe"" ""%1"""; ValueType: string; ValueName: ""; Components: client/pkmn +Root: HKCR; Subkey: ".apred"; ValueData: "{#MyAppName}pkmnrpatch"; Flags: uninsdeletevalue; ValueType: string; ValueName: ""; +Root: HKCR; Subkey: "{#MyAppName}pkmnrpatch"; ValueData: "Archipelago Pokemon Red Patch"; Flags: uninsdeletekey; ValueType: string; ValueName: ""; +Root: HKCR; Subkey: "{#MyAppName}pkmnrpatch\DefaultIcon"; ValueData: "{app}\ArchipelagoPokemonClient.exe,0"; ValueType: string; ValueName: ""; +Root: HKCR; Subkey: "{#MyAppName}pkmnrpatch\shell\open\command"; ValueData: """{app}\ArchipelagoPokemonClient.exe"" ""%1"""; ValueType: string; ValueName: ""; -Root: HKCR; Subkey: ".apblue"; ValueData: "{#MyAppName}pkmnbpatch"; Flags: uninsdeletevalue; ValueType: string; ValueName: ""; Components: client/pkmn -Root: HKCR; Subkey: "{#MyAppName}pkmnbpatch"; ValueData: "Archipelago Pokemon Blue Patch"; Flags: uninsdeletekey; ValueType: string; ValueName: ""; Components: client/pkmn -Root: HKCR; Subkey: "{#MyAppName}pkmnbpatch\DefaultIcon"; ValueData: "{app}\ArchipelagoPokemonClient.exe,0"; ValueType: string; ValueName: ""; Components: client/pkmn -Root: HKCR; Subkey: "{#MyAppName}pkmnbpatch\shell\open\command"; ValueData: """{app}\ArchipelagoPokemonClient.exe"" ""%1"""; ValueType: string; ValueName: ""; Components: client/pkmn +Root: HKCR; Subkey: ".apblue"; ValueData: "{#MyAppName}pkmnbpatch"; Flags: uninsdeletevalue; ValueType: string; ValueName: ""; +Root: HKCR; Subkey: "{#MyAppName}pkmnbpatch"; ValueData: "Archipelago Pokemon Blue Patch"; Flags: uninsdeletekey; ValueType: string; ValueName: ""; +Root: HKCR; Subkey: "{#MyAppName}pkmnbpatch\DefaultIcon"; ValueData: "{app}\ArchipelagoPokemonClient.exe,0"; ValueType: string; ValueName: ""; +Root: HKCR; Subkey: "{#MyAppName}pkmnbpatch\shell\open\command"; ValueData: """{app}\ArchipelagoPokemonClient.exe"" ""%1"""; ValueType: string; ValueName: ""; -Root: HKCR; Subkey: ".apbn3"; ValueData: "{#MyAppName}bn3bpatch"; Flags: uninsdeletevalue; ValueType: string; ValueName: ""; Components: client/mmbn3 -Root: HKCR; Subkey: "{#MyAppName}bn3bpatch"; ValueData: "Archipelago MegaMan Battle Network 3 Patch"; Flags: uninsdeletekey; ValueType: string; ValueName: ""; Components: client/mmbn3 -Root: HKCR; Subkey: "{#MyAppName}bn3bpatch\DefaultIcon"; ValueData: "{app}\ArchipelagoMMBN3Client.exe,0"; ValueType: string; ValueName: ""; Components: client/mmbn3 -Root: HKCR; Subkey: "{#MyAppName}bn3bpatch\shell\open\command"; ValueData: """{app}\ArchipelagoMMBN3Client.exe"" ""%1"""; ValueType: string; ValueName: ""; Components: client/mmbn3 +Root: HKCR; Subkey: ".apbn3"; ValueData: "{#MyAppName}bn3bpatch"; Flags: uninsdeletevalue; ValueType: string; ValueName: ""; +Root: HKCR; Subkey: "{#MyAppName}bn3bpatch"; ValueData: "Archipelago MegaMan Battle Network 3 Patch"; Flags: uninsdeletekey; ValueType: string; ValueName: ""; +Root: HKCR; Subkey: "{#MyAppName}bn3bpatch\DefaultIcon"; ValueData: "{app}\ArchipelagoMMBN3Client.exe,0"; ValueType: string; ValueName: ""; +Root: HKCR; Subkey: "{#MyAppName}bn3bpatch\shell\open\command"; ValueData: """{app}\ArchipelagoMMBN3Client.exe"" ""%1"""; ValueType: string; ValueName: ""; -Root: HKCR; Subkey: ".apladx"; ValueData: "{#MyAppName}ladxpatch"; Flags: uninsdeletevalue; ValueType: string; ValueName: ""; Components: client/ladx -Root: HKCR; Subkey: "{#MyAppName}ladxpatch"; ValueData: "Archipelago Links Awakening DX Patch"; Flags: uninsdeletekey; ValueType: string; ValueName: ""; Components: client/ladx -Root: HKCR; Subkey: "{#MyAppName}ladxpatch\DefaultIcon"; ValueData: "{app}\ArchipelagoLinksAwakeningClient.exe,0"; ValueType: string; ValueName: ""; Components: client/ladx -Root: HKCR; Subkey: "{#MyAppName}ladxpatch\shell\open\command"; ValueData: """{app}\ArchipelagoLinksAwakeningClient.exe"" ""%1"""; ValueType: string; ValueName: ""; Components: client/ladx +Root: HKCR; Subkey: ".apladx"; ValueData: "{#MyAppName}ladxpatch"; Flags: uninsdeletevalue; ValueType: string; ValueName: ""; +Root: HKCR; Subkey: "{#MyAppName}ladxpatch"; ValueData: "Archipelago Links Awakening DX Patch"; Flags: uninsdeletekey; ValueType: string; ValueName: ""; +Root: HKCR; Subkey: "{#MyAppName}ladxpatch\DefaultIcon"; ValueData: "{app}\ArchipelagoLinksAwakeningClient.exe,0"; ValueType: string; ValueName: ""; +Root: HKCR; Subkey: "{#MyAppName}ladxpatch\shell\open\command"; ValueData: """{app}\ArchipelagoLinksAwakeningClient.exe"" ""%1"""; ValueType: string; ValueName: ""; -Root: HKCR; Subkey: ".aptloz"; ValueData: "{#MyAppName}tlozpatch"; Flags: uninsdeletevalue; ValueType: string; ValueName: ""; Components: client/tloz -Root: HKCR; Subkey: "{#MyAppName}tlozpatch"; ValueData: "Archipelago The Legend of Zelda Patch"; Flags: uninsdeletekey; ValueType: string; ValueName: ""; Components: client/tloz -Root: HKCR; Subkey: "{#MyAppName}tlozpatch\DefaultIcon"; ValueData: "{app}\ArchipelagoZelda1Client.exe,0"; ValueType: string; ValueName: ""; Components: client/tloz -Root: HKCR; Subkey: "{#MyAppName}tlozpatch\shell\open\command"; ValueData: """{app}\ArchipelagoZelda1Client.exe"" ""%1"""; ValueType: string; ValueName: ""; Components: client/tloz +Root: HKCR; Subkey: ".aptloz"; ValueData: "{#MyAppName}tlozpatch"; Flags: uninsdeletevalue; ValueType: string; ValueName: ""; +Root: HKCR; Subkey: "{#MyAppName}tlozpatch"; ValueData: "Archipelago The Legend of Zelda Patch"; Flags: uninsdeletekey; ValueType: string; ValueName: ""; +Root: HKCR; Subkey: "{#MyAppName}tlozpatch\DefaultIcon"; ValueData: "{app}\ArchipelagoZelda1Client.exe,0"; ValueType: string; ValueName: ""; +Root: HKCR; Subkey: "{#MyAppName}tlozpatch\shell\open\command"; ValueData: """{app}\ArchipelagoZelda1Client.exe"" ""%1"""; ValueType: string; ValueName: ""; -Root: HKCR; Subkey: ".apadvn"; ValueData: "{#MyAppName}advnpatch"; Flags: uninsdeletevalue; ValueType: string; ValueName: ""; Components: client/advn -Root: HKCR; Subkey: "{#MyAppName}advnpatch"; ValueData: "Archipelago Adventure Patch"; Flags: uninsdeletekey; ValueType: string; ValueName: ""; Components: client/advn -Root: HKCR; Subkey: "{#MyAppName}advnpatch\DefaultIcon"; ValueData: "{app}\ArchipelagoAdventureClient.exe,0"; ValueType: string; ValueName: ""; Components: client/advn -Root: HKCR; Subkey: "{#MyAppName}advnpatch\shell\open\command"; ValueData: """{app}\ArchipelagoAdventureClient.exe"" ""%1"""; ValueType: string; ValueName: ""; Components: client/advn +Root: HKCR; Subkey: ".apadvn"; ValueData: "{#MyAppName}advnpatch"; Flags: uninsdeletevalue; ValueType: string; ValueName: ""; +Root: HKCR; Subkey: "{#MyAppName}advnpatch"; ValueData: "Archipelago Adventure Patch"; Flags: uninsdeletekey; ValueType: string; ValueName: ""; +Root: HKCR; Subkey: "{#MyAppName}advnpatch\DefaultIcon"; ValueData: "{app}\ArchipelagoAdventureClient.exe,0"; ValueType: string; ValueName: ""; +Root: HKCR; Subkey: "{#MyAppName}advnpatch\shell\open\command"; ValueData: """{app}\ArchipelagoAdventureClient.exe"" ""%1"""; ValueType: string; ValueName: ""; -Root: HKCR; Subkey: ".archipelago"; ValueData: "{#MyAppName}multidata"; Flags: uninsdeletevalue; ValueType: string; ValueName: ""; Components: server -Root: HKCR; Subkey: "{#MyAppName}multidata"; ValueData: "Archipelago Server Data"; Flags: uninsdeletekey; ValueType: string; ValueName: ""; Components: server -Root: HKCR; Subkey: "{#MyAppName}multidata\DefaultIcon"; ValueData: "{app}\ArchipelagoServer.exe,0"; ValueType: string; ValueName: ""; Components: server -Root: HKCR; Subkey: "{#MyAppName}multidata\shell\open\command"; ValueData: """{app}\ArchipelagoServer.exe"" ""%1"""; ValueType: string; ValueName: ""; Components: server +Root: HKCR; Subkey: ".archipelago"; ValueData: "{#MyAppName}multidata"; Flags: uninsdeletevalue; ValueType: string; ValueName: ""; +Root: HKCR; Subkey: "{#MyAppName}multidata"; ValueData: "Archipelago Server Data"; Flags: uninsdeletekey; ValueType: string; ValueName: ""; +Root: HKCR; Subkey: "{#MyAppName}multidata\DefaultIcon"; ValueData: "{app}\ArchipelagoServer.exe,0"; ValueType: string; ValueName: ""; +Root: HKCR; Subkey: "{#MyAppName}multidata\shell\open\command"; ValueData: """{app}\ArchipelagoServer.exe"" ""%1"""; ValueType: string; ValueName: ""; -Root: HKCR; Subkey: "archipelago"; ValueType: "string"; ValueData: "Archipegalo Protocol"; Flags: uninsdeletekey; Components: client/text -Root: HKCR; Subkey: "archipelago"; ValueType: "string"; ValueName: "URL Protocol"; ValueData: ""; Components: client/text -Root: HKCR; Subkey: "archipelago\DefaultIcon"; ValueType: "string"; ValueData: "{app}\ArchipelagoTextClient.exe,0"; Components: client/text -Root: HKCR; Subkey: "archipelago\shell\open\command"; ValueType: "string"; ValueData: """{app}\ArchipelagoTextClient.exe"" ""%1"""; Components: client/text +Root: HKCR; Subkey: "archipelago"; ValueType: "string"; ValueData: "Archipegalo Protocol"; Flags: uninsdeletekey; +Root: HKCR; Subkey: "archipelago"; ValueType: "string"; ValueName: "URL Protocol"; ValueData: ""; +Root: HKCR; Subkey: "archipelago\DefaultIcon"; ValueType: "string"; ValueData: "{app}\ArchipelagoTextClient.exe,0"; +Root: HKCR; Subkey: "archipelago\shell\open\command"; ValueType: "string"; ValueData: """{app}\ArchipelagoTextClient.exe"" ""%1"""; [Code] -const - SHCONTCH_NOPROGRESSBOX = 4; - SHCONTCH_RESPONDYESTOALL = 16; - // See: https://stackoverflow.com/a/51614652/2287576 function IsVCRedist64BitNeeded(): boolean; var @@ -320,594 +198,3 @@ begin Result := True; end; end; - -var R : longint; - -var lttprom: string; -var LttPROMFilePage: TInputFileWizardPage; - -var smrom: string; -var SMRomFilePage: TInputFileWizardPage; - -var dkc3rom: string; -var DKC3RomFilePage: TInputFileWizardPage; - -var smwrom: string; -var SMWRomFilePage: TInputFileWizardPage; - -var soerom: string; -var SoERomFilePage: TInputFileWizardPage; - -var l2acrom: string; -var L2ACROMFilePage: TInputFileWizardPage; - -var ootrom: string; -var OoTROMFilePage: TInputFileWizardPage; - -var zlrom: string; -var ZlROMFilePage: TInputFileWizardPage; - -var redrom: string; -var RedROMFilePage: TInputFileWizardPage; - -var bluerom: string; -var BlueROMFilePage: TInputFileWizardPage; - -var bn3rom: string; -var BN3ROMFilePage: TInputFileWizardPage; - -var ladxrom: string; -var LADXROMFilePage: TInputFileWizardPage; - -var tlozrom: string; -var TLoZROMFilePage: TInputFileWizardPage; - -var advnrom: string; -var AdvnROMFilePage: TInputFileWizardPage; - -function GetSNESMD5OfFile(const rom: string): string; -var data: AnsiString; -begin - if LoadStringFromFile(rom, data) then - begin - if Length(data) mod 1024 = 512 then - begin - data := copy(data, 513, Length(data)-512); - end; - Result := GetMD5OfString(data); - end; -end; - -function GetSMSMD5OfFile(const rom: string): string; -var data: AnsiString; -begin - if LoadStringFromFile(rom, data) then - begin - Result := GetMD5OfString(data); - end; -end; - -function CheckRom(name: string; hash: string): string; -var rom: string; -begin - log('Handling ' + name) - rom := FileSearch(name, WizardDirValue()); - if Length(rom) > 0 then - begin - log('existing ROM found'); - log(IntToStr(CompareStr(GetSNESMD5OfFile(rom), hash))); - if CompareStr(GetSNESMD5OfFile(rom), hash) = 0 then - begin - log('existing ROM verified'); - Result := rom; - exit; - end; - log('existing ROM failed verification'); - end; -end; - -function CheckSMSRom(name: string; hash: string): string; -var rom: string; -begin - log('Handling ' + name) - rom := FileSearch(name, WizardDirValue()); - if Length(rom) > 0 then - begin - log('existing ROM found'); - log(IntToStr(CompareStr(GetSMSMD5OfFile(rom), hash))); - if CompareStr(GetSMSMD5OfFile(rom), hash) = 0 then - begin - log('existing ROM verified'); - Result := rom; - exit; - end; - log('existing ROM failed verification'); - end; -end; - -function CheckNESRom(name: string; hash: string): string; -var rom: string; -begin - log('Handling ' + name) - rom := FileSearch(name, WizardDirValue()); - if Length(rom) > 0 then - begin - log('existing ROM found'); - log(IntToStr(CompareStr(GetSMSMD5OfFile(rom), hash))); - if CompareStr(GetSMSMD5OfFile(rom), hash) = 0 then - begin - log('existing ROM verified'); - Result := rom; - exit; - end; - log('existing ROM failed verification'); - end; -end; - -function AddRomPage(name: string): TInputFileWizardPage; -begin - Result := - CreateInputFilePage( - wpSelectComponents, - 'Select ROM File', - 'Where is your ' + name + ' located?', - 'Select the file, then click Next.'); - - Result.Add( - 'Location of ROM file:', - 'SNES ROM files|*.sfc;*.smc|All files|*.*', - '.sfc'); -end; - - -function AddGBRomPage(name: string): TInputFileWizardPage; -begin - Result := - CreateInputFilePage( - wpSelectComponents, - 'Select ROM File', - 'Where is your ' + name + ' located?', - 'Select the file, then click Next.'); - - Result.Add( - 'Location of ROM file:', - 'GB ROM files|*.gb;*.gbc|All files|*.*', - '.gb'); -end; - -function AddGBARomPage(name: string): TInputFileWizardPage; -begin - Result := - CreateInputFilePage( - wpSelectComponents, - 'Select ROM File', - 'Where is your ' + name + ' located?', - 'Select the file, then click Next.'); - Result.Add( - 'Location of ROM file:', - 'GBA ROM files|*.gba|All files|*.*', - '.gba'); -end; - -function AddSMSRomPage(name: string): TInputFileWizardPage; -begin - Result := - CreateInputFilePage( - wpSelectComponents, - 'Select ROM File', - 'Where is your ' + name + ' located?', - 'Select the file, then click Next.'); - Result.Add( - 'Location of ROM file:', - 'SMS ROM files|*.sms|All files|*.*', - '.sms'); -end; - -function AddNESRomPage(name: string): TInputFileWizardPage; -begin - Result := - CreateInputFilePage( - wpSelectComponents, - 'Select ROM File', - 'Where is your ' + name + ' located?', - 'Select the file, then click Next.'); - - Result.Add( - 'Location of ROM file:', - 'NES ROM files|*.nes|All files|*.*', - '.nes'); -end; - -procedure AddOoTRomPage(); -begin - ootrom := FileSearch('The Legend of Zelda - Ocarina of Time.z64', WizardDirValue()); - if Length(ootrom) > 0 then - begin - log('existing ROM found'); - log(IntToStr(CompareStr(GetMD5OfFile(ootrom), '5bd1fe107bf8106b2ab6650abecd54d6'))); // normal - log(IntToStr(CompareStr(GetMD5OfFile(ootrom), '6697768a7a7df2dd27a692a2638ea90b'))); // byteswapped - log(IntToStr(CompareStr(GetMD5OfFile(ootrom), '05f0f3ebacbc8df9243b6148ffe4792f'))); // decompressed - if (CompareStr(GetMD5OfFile(ootrom), '5bd1fe107bf8106b2ab6650abecd54d6') = 0) or (CompareStr(GetMD5OfFile(ootrom), '6697768a7a7df2dd27a692a2638ea90b') = 0) or (CompareStr(GetMD5OfFile(ootrom), '05f0f3ebacbc8df9243b6148ffe4792f') = 0) then - begin - log('existing ROM verified'); - exit; - end; - log('existing ROM failed verification'); - end; - ootrom := '' - OoTROMFilePage := - CreateInputFilePage( - wpSelectComponents, - 'Select ROM File', - 'Where is your OoT 1.0 ROM located?', - 'Select the file, then click Next.'); - - OoTROMFilePage.Add( - 'Location of ROM file:', - 'N64 ROM files (*.z64, *.n64)|*.z64;*.n64|All files|*.*', - '.z64'); -end; - -function AddA26Page(name: string): TInputFileWizardPage; -begin - Result := - CreateInputFilePage( - wpSelectComponents, - 'Select ROM File', - 'Where is your ' + name + ' located?', - 'Select the file, then click Next.'); - - Result.Add( - 'Location of ROM file:', - 'A2600 ROM files|*.BIN;*.a26|All files|*.*', - '.BIN'); -end; - -function NextButtonClick(CurPageID: Integer): Boolean; -begin - 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] = '') - else if (assigned(DKC3ROMFilePage)) and (CurPageID = DKC3ROMFilePage.ID) then - Result := not (DKC3ROMFilePage.Values[0] = '') - else if (assigned(SMWROMFilePage)) and (CurPageID = SMWROMFilePage.ID) then - Result := not (SMWROMFilePage.Values[0] = '') - else if (assigned(SoEROMFilePage)) and (CurPageID = SoEROMFilePage.ID) then - Result := not (SoEROMFilePage.Values[0] = '') - else if (assigned(L2ACROMFilePage)) and (CurPageID = L2ACROMFilePage.ID) then - Result := not (L2ACROMFilePage.Values[0] = '') - else if (assigned(OoTROMFilePage)) and (CurPageID = OoTROMFilePage.ID) then - Result := not (OoTROMFilePage.Values[0] = '') - else if (assigned(BN3ROMFilePage)) and (CurPageID = BN3ROMFilePage.ID) then - Result := not (BN3ROMFilePage.Values[0] = '') - else if (assigned(ZlROMFilePage)) and (CurPageID = ZlROMFilePage.ID) then - Result := not (ZlROMFilePage.Values[0] = '') - else if (assigned(RedROMFilePage)) and (CurPageID = RedROMFilePage.ID) then - Result := not (RedROMFilePage.Values[0] = '') - else if (assigned(BlueROMFilePage)) and (CurPageID = BlueROMFilePage.ID) then - Result := not (BlueROMFilePage.Values[0] = '') - else if (assigned(LADXROMFilePage)) and (CurPageID = LADXROMFilePage.ID) then - Result := not (LADXROMFilePage.Values[0] = '') - else if (assigned(TLoZROMFilePage)) and (CurPageID = TLoZROMFilePage.ID) then - Result := not (TLoZROMFilePage.Values[0] = '') - else if (assigned(AdvnROMFilePage)) and (CurPageID = AdvnROMFilePage.ID) then - Result := not (AdvnROMFilePage.Values[0] = '') - else - Result := True; -end; - -function GetROMPath(Param: string): string; -begin - if Length(lttprom) > 0 then - Result := lttprom - else if Assigned(LttPRomFilePage) then - 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 - Result := ''; - end; - -function GetSMROMPath(Param: string): string; -begin - if Length(smrom) > 0 then - Result := smrom - else if Assigned(SMRomFilePage) then - begin - R := CompareStr(GetSNESMD5OfFile(SMROMFilePage.Values[0]), '21f3e98df4780ee1c667b84e57d88675') - if R <> 0 then - MsgBox('Super Metroid ROM validation failed. Very likely wrong file.', mbInformation, MB_OK); - - Result := SMROMFilePage.Values[0] - end - else - Result := ''; - end; - -function GetDKC3ROMPath(Param: string): string; -begin - if Length(dkc3rom) > 0 then - Result := dkc3rom - else if Assigned(DKC3RomFilePage) then - begin - R := CompareStr(GetSNESMD5OfFile(DKC3ROMFilePage.Values[0]), '120abf304f0c40fe059f6a192ed4f947') - if R <> 0 then - MsgBox('Donkey Kong Country 3 ROM validation failed. Very likely wrong file.', mbInformation, MB_OK); - - Result := DKC3ROMFilePage.Values[0] - end - else - Result := ''; - end; - -function GetSMWROMPath(Param: string): string; -begin - if Length(smwrom) > 0 then - Result := smwrom - else if Assigned(SMWRomFilePage) then - begin - R := CompareStr(GetSNESMD5OfFile(SMWROMFilePage.Values[0]), 'cdd3c8c37322978ca8669b34bc89c804') - if R <> 0 then - MsgBox('Super Mario World ROM validation failed. Very likely wrong file.', mbInformation, MB_OK); - - Result := SMWROMFilePage.Values[0] - end - else - Result := ''; - end; - -function GetSoEROMPath(Param: string): string; -begin - if Length(soerom) > 0 then - Result := soerom - else if Assigned(SoERomFilePage) then - begin - R := CompareStr(GetSNESMD5OfFile(SoEROMFilePage.Values[0]), '6e9c94511d04fac6e0a1e582c170be3a') - if R <> 0 then - MsgBox('Secret of Evermore ROM validation failed. Very likely wrong file.', mbInformation, MB_OK); - - Result := SoEROMFilePage.Values[0] - end - else - Result := ''; - end; - -function GetOoTROMPath(Param: string): string; -begin - if Length(ootrom) > 0 then - Result := ootrom - 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 - MsgBox('OoT ROM validation failed. Very likely wrong file.', mbInformation, MB_OK); - - Result := OoTROMFilePage.Values[0] - end - else - Result := ''; -end; - -function GetL2ACROMPath(Param: string): string; -begin - if Length(l2acrom) > 0 then - Result := l2acrom - else if Assigned(L2ACROMFilePage) then - begin - R := CompareStr(GetSNESMD5OfFile(L2ACROMFilePage.Values[0]), '6efc477d6203ed2b3b9133c1cd9e9c5d') - if R <> 0 then - MsgBox('Lufia II ROM validation failed. Very likely wrong file.', mbInformation, MB_OK); - - Result := L2ACROMFilePage.Values[0] - end - else - Result := ''; -end; - -function GetZlROMPath(Param: string): string; -begin - if Length(zlrom) > 0 then - Result := zlrom - else if Assigned(ZlROMFilePage) then - begin - R := CompareStr(GetMD5OfFile(ZlROMFilePage.Values[0]), 'd4bf9e7bcf9a48da53785d2ae7bc4270'); - if R <> 0 then - MsgBox('Zillion ROM validation failed. Very likely wrong file.', mbInformation, MB_OK); - - Result := ZlROMFilePage.Values[0] - end - else - Result := ''; -end; - -function GetRedROMPath(Param: string): string; -begin - if Length(redrom) > 0 then - Result := redrom - else if Assigned(RedROMFilePage) then - begin - R := CompareStr(GetMD5OfFile(RedROMFilePage.Values[0]), '3d45c1ee9abd5738df46d2bdda8b57dc') - if R <> 0 then - MsgBox('Pokemon Red ROM validation failed. Very likely wrong file.', mbInformation, MB_OK); - - Result := RedROMFilePage.Values[0] - end - else - Result := ''; - end; - -function GetBlueROMPath(Param: string): string; -begin - if Length(bluerom) > 0 then - Result := bluerom - else if Assigned(BlueROMFilePage) then - begin - R := CompareStr(GetMD5OfFile(BlueROMFilePage.Values[0]), '50927e843568814f7ed45ec4f944bd8b') - if R <> 0 then - MsgBox('Pokemon Blue ROM validation failed. Very likely wrong file.', mbInformation, MB_OK); - - Result := BlueROMFilePage.Values[0] - end - else - Result := ''; - end; - -function GetTLoZROMPath(Param: string): string; -begin - if Length(tlozrom) > 0 then - Result := tlozrom - else if Assigned(TLoZROMFilePage) then - begin - R := CompareStr(GetMD5OfFile(TLoZROMFilePage.Values[0]), '337bd6f1a1163df31bf2633665589ab0'); - if R <> 0 then - MsgBox('The Legend of Zelda ROM validation failed. Very likely wrong file.', mbInformation, MB_OK); - - Result := TLoZROMFilePage.Values[0] - end - else - Result := ''; -end; - -function GetLADXROMPath(Param: string): string; -begin - if Length(ladxrom) > 0 then - Result := ladxrom - else if Assigned(LADXROMFilePage) then - begin - R := CompareStr(GetMD5OfFile(LADXROMFilePage.Values[0]), '07c211479386825042efb4ad31bb525f') - if R <> 0 then - MsgBox('Link''s Awakening DX ROM validation failed. Very likely wrong file.', mbInformation, MB_OK); - - Result := LADXROMFilePage.Values[0] - end - else - Result := ''; - end; - -function GetAdvnROMPath(Param: string): string; -begin - if Length(advnrom) > 0 then - Result := advnrom - else if Assigned(AdvnROMFilePage) then - begin - R := CompareStr(GetMD5OfFile(AdvnROMFilePage.Values[0]), '157bddb7192754a45372be196797f284'); - if R <> 0 then - MsgBox('Adventure ROM validation failed. Very likely wrong file.', mbInformation, MB_OK); - - Result := AdvnROMFilePage.Values[0] - end - else - Result := ''; -end; - -function GetBN3ROMPath(Param: string): string; -begin - if Length(bn3rom) > 0 then - Result := bn3rom - else if Assigned(BN3ROMFilePage) then - begin - R := CompareStr(GetMD5OfFile(BN3ROMFilePage.Values[0]), '6fe31df0144759b34ad666badaacc442') - if R <> 0 then - MsgBox('MegaMan Battle Network 3 Blue ROM validation failed. Very likely wrong file.', mbInformation, MB_OK); - - Result := BN3ROMFilePage.Values[0] - end - else - Result := ''; - end; - -procedure InitializeWizard(); -begin - AddOoTRomPage(); - - lttprom := CheckRom('Zelda no Densetsu - Kamigami no Triforce (Japan).sfc', '03a63945398191337e896e5771f77173'); - if Length(lttprom) = 0 then - LttPROMFilePage:= AddRomPage('Zelda no Densetsu - Kamigami no Triforce (Japan).sfc'); - - smrom := CheckRom('Super Metroid (JU).sfc', '21f3e98df4780ee1c667b84e57d88675'); - if Length(smrom) = 0 then - SMRomFilePage:= AddRomPage('Super Metroid (JU).sfc'); - - dkc3rom := CheckRom('Donkey Kong Country 3 - Dixie Kong''s Double Trouble! (USA) (En,Fr).sfc', '120abf304f0c40fe059f6a192ed4f947'); - if Length(dkc3rom) = 0 then - DKC3RomFilePage:= AddRomPage('Donkey Kong Country 3 - Dixie Kong''s Double Trouble! (USA) (En,Fr).sfc'); - - smwrom := CheckRom('Super Mario World (USA).sfc', 'cdd3c8c37322978ca8669b34bc89c804'); - if Length(smwrom) = 0 then - SMWRomFilePage:= AddRomPage('Super Mario World (USA).sfc'); - - soerom := CheckRom('Secret of Evermore (USA).sfc', '6e9c94511d04fac6e0a1e582c170be3a'); - if Length(soerom) = 0 then - SoEROMFilePage:= AddRomPage('Secret of Evermore (USA).sfc'); - - zlrom := CheckSMSRom('Zillion (UE) [!].sms', 'd4bf9e7bcf9a48da53785d2ae7bc4270'); - if Length(zlrom) = 0 then - ZlROMFilePage:= AddSMSRomPage('Zillion (UE) [!].sms'); - - redrom := CheckRom('Pokemon Red (UE) [S][!].gb','3d45c1ee9abd5738df46d2bdda8b57dc'); - if Length(redrom) = 0 then - RedROMFilePage:= AddGBRomPage('Pokemon Red (UE) [S][!].gb'); - - bluerom := CheckRom('Pokemon Blue (UE) [S][!].gb','50927e843568814f7ed45ec4f944bd8b'); - if Length(bluerom) = 0 then - BlueROMFilePage:= AddGBRomPage('Pokemon Blue (UE) [S][!].gb'); - - bn3rom := CheckRom('Mega Man Battle Network 3 - Blue Version (USA).gba','6fe31df0144759b34ad666badaacc442'); - if Length(bn3rom) = 0 then - BN3ROMFilePage:= AddGBARomPage('Mega Man Battle Network 3 - Blue Version (USA).gba'); - - ladxrom := CheckRom('Legend of Zelda, The - Link''s Awakening DX (USA, Europe) (SGB Enhanced).gbc','07c211479386825042efb4ad31bb525f'); - if Length(ladxrom) = 0 then - LADXROMFilePage:= AddGBRomPage('Legend of Zelda, The - Link''s Awakening DX (USA, Europe) (SGB Enhanced).gbc'); - - l2acrom := CheckRom('Lufia II - Rise of the Sinistrals (USA).sfc', '6efc477d6203ed2b3b9133c1cd9e9c5d'); - if Length(l2acrom) = 0 then - L2ACROMFilePage:= AddRomPage('Lufia II - Rise of the Sinistrals (USA).sfc'); - - tlozrom := CheckNESROM('Legend of Zelda, The (U) (PRG0) [!].nes', '337bd6f1a1163df31bf2633665589ab0'); - if Length(tlozrom) = 0 then - TLoZROMFilePage:= AddNESRomPage('Legend of Zelda, The (U) (PRG0) [!].nes'); - - advnrom := CheckSMSRom('ADVNTURE.BIN', '157bddb7192754a45372be196797f284'); - if Length(advnrom) = 0 then - AdvnROMFilePage:= AddA26Page('ADVNTURE.BIN'); -end; - - -function ShouldSkipPage(PageID: Integer): Boolean; -begin - Result := False; - if (assigned(LttPROMFilePage)) and (PageID = LttPROMFilePage.ID) then - Result := not (WizardIsComponentSelected('client/sni/lttp') or WizardIsComponentSelected('generator/lttp')); - if (assigned(SMROMFilePage)) and (PageID = SMROMFilePage.ID) then - Result := not (WizardIsComponentSelected('client/sni/sm') or WizardIsComponentSelected('generator/sm')); - if (assigned(DKC3ROMFilePage)) and (PageID = DKC3ROMFilePage.ID) then - Result := not (WizardIsComponentSelected('client/sni/dkc3') or WizardIsComponentSelected('generator/dkc3')); - if (assigned(SMWROMFilePage)) and (PageID = SMWROMFilePage.ID) then - Result := not (WizardIsComponentSelected('client/sni/smw') or WizardIsComponentSelected('generator/smw')); - if (assigned(L2ACROMFilePage)) and (PageID = L2ACROMFilePage.ID) then - Result := not (WizardIsComponentSelected('client/sni/l2ac') or WizardIsComponentSelected('generator/l2ac')); - if (assigned(SoEROMFilePage)) and (PageID = SoEROMFilePage.ID) then - Result := not (WizardIsComponentSelected('generator/soe')); - if (assigned(OoTROMFilePage)) and (PageID = OoTROMFilePage.ID) then - Result := not (WizardIsComponentSelected('generator/oot') or WizardIsComponentSelected('client/oot')); - if (assigned(ZlROMFilePage)) and (PageID = ZlROMFilePage.ID) then - Result := not (WizardIsComponentSelected('generator/zl') or WizardIsComponentSelected('client/zl')); - if (assigned(RedROMFilePage)) and (PageID = RedROMFilePage.ID) then - Result := not (WizardIsComponentSelected('generator/pkmn_r') or WizardIsComponentSelected('client/pkmn/red')); - if (assigned(BlueROMFilePage)) and (PageID = BlueROMFilePage.ID) then - Result := not (WizardIsComponentSelected('generator/pkmn_b') or WizardIsComponentSelected('client/pkmn/blue')); - if (assigned(BN3ROMFilePage)) and (PageID = BN3ROMFilePage.ID) then - Result := not (WizardIsComponentSelected('generator/mmbn3') or WizardIsComponentSelected('client/mmbn3')); - if (assigned(LADXROMFilePage)) and (PageID = LADXROMFilePage.ID) then - Result := not (WizardIsComponentSelected('generator/ladx') or WizardIsComponentSelected('client/ladx')); - if (assigned(TLoZROMFilePage)) and (PageID = TLoZROMFilePage.ID) then - Result := not (WizardIsComponentSelected('generator/tloz') or WizardIsComponentSelected('client/tloz')); - if (assigned(AdvnROMFilePage)) and (PageID = AdvnROMFilePage.ID) then - Result := not (WizardIsComponentSelected('client/advn')); -end; From 8109d4a1af1b0c10acabebb4e5613ddee1f9fd64 Mon Sep 17 00:00:00 2001 From: el-u <109771707+el-u@users.noreply.github.com> Date: Mon, 23 Oct 2023 22:20:27 +0200 Subject: [PATCH 33/54] lufia2ac: prevent "door stairs" and "rare stairs" (#2341) --- worlds/lufia2ac/basepatch/basepatch.asm | 47 ++++++++++++++++++ worlds/lufia2ac/basepatch/basepatch.bsdiff4 | Bin 8555 -> 8638 bytes .../lufia2ac/docs/en_Lufia II Ancient Cave.md | 2 +- 3 files changed, 48 insertions(+), 1 deletion(-) diff --git a/worlds/lufia2ac/basepatch/basepatch.asm b/worlds/lufia2ac/basepatch/basepatch.asm index 923ee6a226..f298a1129d 100644 --- a/worlds/lufia2ac/basepatch/basepatch.asm +++ b/worlds/lufia2ac/basepatch/basepatch.asm @@ -1127,6 +1127,53 @@ pullpc +; door stairs fix +pushpc +org $839453 + ; DB=$7F, x=0, m=1 + JSL DoorStairsFix ; overwrites JSR $9B18 : JSR $9D11 + NOP #2 +pullpc + +DoorStairsFix: + CLC + LDY.w #$0000 +--: LDX.w #$00FF ; loop through floor layout starting from the bottom right +-: LDA $EA00,X ; read node contents + BEQ + ; always skip empty nodes + BCC ++ ; 1st pass: skip all blocked nodes (would cause door stairs or rare stairs) + LDA $E9F0,X ; 2nd pass: skip only if the one above is also blocked (would cause door stairs) +++: BMI + + INY ; count usable nodes ++: DEX + BPL - + TYA + BNE ++ ; all nodes blocked? + SEC ; set up 2nd, less restrictive pass + BRA -- +++: JSL $8082C7 ; advance RNG + STA $00211B + TDC + STA $00211B ; M7A; first factor = random number from 0 to 255 + TYA + STA $00211C ; M7B; second factor = number of possible stair positions + LDA $002135 ; MPYM; calculate random number from 0 to number of possible stair positions - 1 + TAY + LDX.w #$00FF ; loop through floor layout starting from the bottom right +-: LDA $EA00,X ; read node contents + BEQ + ; always skip empty nodes + BCC ++ ; if 1st pass was sufficient: skip all blocked nodes (prevent door stairs and rare stairs) + LDA $E9F0,X ; if 2nd pass was needed: skip only if the one above is also blocked (prevent door stairs) +++: BMI + + DEY ; count down to locate the (Y+1)th usable node + BMI ++ ++: DEX + BPL - +++: TXA ; return selected stair node coordinate + RTL + + + ; equipment text fix pushpc org $81F2E3 diff --git a/worlds/lufia2ac/basepatch/basepatch.bsdiff4 b/worlds/lufia2ac/basepatch/basepatch.bsdiff4 index aee1c7125ddac1ca5ccd03a8caf5fa563f2ccee9..4ed1815039a04c4f3c1ce8a6c5a28ddfda86f96e 100644 GIT binary patch literal 8638 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 diff --git a/worlds/lufia2ac/docs/en_Lufia II Ancient Cave.md b/worlds/lufia2ac/docs/en_Lufia II Ancient Cave.md index 849a9f9c9d..d24c4ef9f9 100644 --- a/worlds/lufia2ac/docs/en_Lufia II Ancient Cave.md +++ b/worlds/lufia2ac/docs/en_Lufia II Ancient Cave.md @@ -76,7 +76,7 @@ Your Party Leader will hold up the item they received when not in a fight or in ###### Bug fixes: -- Vanilla game bugs that could result in softlocks or save file corruption have been fixed +- Vanilla game bugs that could result in anomalous floors, softlocks, or save file corruption have been fixed - (optional) Bugfix for the algorithm that determines the item pool for red chest gear. Enabling this allows the cave to generate shields, headgear, rings, and jewels in red chests even after floor B9 - (optional) Bugfix for the outlandish cravings of capsule monsters in the US version. Enabling this makes feeding work From 12c73acb20cccfef53d1d205dbf6b8e8d70c1890 Mon Sep 17 00:00:00 2001 From: Justus Lind Date: Tue, 24 Oct 2023 06:39:37 +1000 Subject: [PATCH 34/54] Muse Dash: Make which .net to download more explicit in setup guides. (#2328) --- worlds/musedash/docs/setup_en.md | 4 ++-- worlds/musedash/docs/setup_es.md | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/worlds/musedash/docs/setup_en.md b/worlds/musedash/docs/setup_en.md index 1ab61ff22a..ebf165c7dd 100644 --- a/worlds/musedash/docs/setup_en.md +++ b/worlds/musedash/docs/setup_en.md @@ -8,10 +8,10 @@ - Windows 8 or Newer. - Muse Dash: [Available on Steam](https://store.steampowered.com/app/774171/Muse_Dash/) - - \[Optional\] [Just as Planned] DLC: [Also Available on Steam](https://store.steampowered.com/app/1055810/Muse_Dash__Just_as_planned/) + - \[Optional\] [Muse Plus] DLC: [Also Available on Steam](https://store.steampowered.com/app/2593750/Muse_Dash__Muse_Plus/) - Melon Loader: [GitHub](https://github.com/LavaGang/MelonLoader/releases/latest) - .Net Framework 4.8 may be needed for the installer: [Download](https://dotnet.microsoft.com/en-us/download/dotnet-framework/net48) -- .Net 6.0 (If not already installed): [Download](https://dotnet.microsoft.com/en-us/download/dotnet/6.0#runtime-6.0.15) +- .NET Desktop Runtime 6.0.XX (If not already installed): [Download](https://dotnet.microsoft.com/en-us/download/dotnet/6.0) - Muse Dash Archipelago Mod: [GitHub](https://github.com/DeamonHunter/ArchipelagoMuseDash/releases/latest) ## Installing the Archipelago mod to Muse Dash diff --git a/worlds/musedash/docs/setup_es.md b/worlds/musedash/docs/setup_es.md index 21fc69e7eb..0d737c26d7 100644 --- a/worlds/musedash/docs/setup_es.md +++ b/worlds/musedash/docs/setup_es.md @@ -8,10 +8,10 @@ - Windows 8 o más reciente. - Muse Dash: [Disponible en Steam](https://store.steampowered.com/app/774171/Muse_Dash/) - - \[Opcional\] [Just as Planned] DLC: [tambien disponible on Steam](https://store.steampowered.com/app/1055810/Muse_Dash__Just_as_planned/) + - \[Opcional\] [Muse Plus] DLC: [tambien disponible on Steam](https://store.steampowered.com/app/2593750/Muse_Dash__Muse_Plus/) - Melon Loader: [GitHub](https://github.com/LavaGang/MelonLoader/releases/latest) - - .Net Framework 4.8 podría ser necesario para el instalador: [Descarga](https://dotnet.microsoft.com/en-us/download/dotnet-framework/net48) -- .Net 6.0 (si aún no está instalado): [Descarga](https://dotnet.microsoft.com/en-us/download/dotnet/6.0#runtime-6.0.15) + - .Net Framework 4.8 podría ser necesario para el instalador: [Descarga](https://dotnet.microsoft.com/es-es/download/dotnet-framework/net48) +- Entorno de ejecución de escritorio de .NET 6.0.XX (si aún no está instalado): [Descarga](https://dotnet.microsoft.com/es-es/download/dotnet/6.0) - Muse Dash Archipelago Mod: [GitHub](https://github.com/DeamonHunter/ArchipelagoMuseDash/releases/latest) ## Instalar el mod de Archipelago en Muse Dash From 764128568e0411a032179f7a8515baa006bdc51f Mon Sep 17 00:00:00 2001 From: Aaron Wagener Date: Mon, 23 Oct 2023 19:20:08 -0500 Subject: [PATCH 35/54] WebHost: consistent naming for player options (#2037) * WebHost: unify references to options * it was just an extra s the whole time... * grammar * redirect from old pages * redirect stuff correctly * use url_for * use " for modified strings * remove redirect cache * player_settings * update site map --- Generate.py | 2 +- WebHostLib/misc.py | 28 ++- WebHostLib/options.py | 34 ++-- .../{player-settings.js => player-options.js} | 180 +++++++++--------- ...ighted-settings.js => weighted-options.js} | 2 +- ...player-settings.css => player-options.css} | 68 +++---- ...hted-settings.css => weighted-options.css} | 0 ...ayer-settings.html => player-options.html} | 20 +- WebHostLib/templates/siteMap.html | 6 +- WebHostLib/templates/supportedGames.html | 8 +- ...ed-settings.html => weighted-options.html} | 14 +- worlds/AutoWorld.py | 2 +- worlds/bk_sudoku/__init__.py | 2 +- worlds/ff1/__init__.py | 2 +- 14 files changed, 190 insertions(+), 178 deletions(-) rename WebHostLib/static/assets/{player-settings.js => player-options.js} (63%) rename WebHostLib/static/assets/{weighted-settings.js => weighted-options.js} (99%) rename WebHostLib/static/styles/{player-settings.css => player-options.css} (67%) rename WebHostLib/static/styles/{weighted-settings.css => weighted-options.css} (100%) rename WebHostLib/templates/{player-settings.html => player-options.html} (75%) rename WebHostLib/templates/{weighted-settings.html => weighted-options.html} (82%) diff --git a/Generate.py b/Generate.py index 08fe2b9083..34a0084e8d 100644 --- a/Generate.py +++ b/Generate.py @@ -169,7 +169,7 @@ def main(args=None, callback=ERmain): for player in range(1, args.multi + 1): player_path_cache[player] = player_files.get(player, args.weights_file_path) name_counter = Counter() - erargs.player_settings = {} + erargs.player_options = {} player = 1 while player <= args.multi: diff --git a/WebHostLib/misc.py b/WebHostLib/misc.py index e3111ed5b5..ee04e56fd7 100644 --- a/WebHostLib/misc.py +++ b/WebHostLib/misc.py @@ -37,17 +37,29 @@ def start_playing(): return render_template(f"startPlaying.html") -@app.route('/weighted-settings') -@cache.cached() +# TODO for back compat. remove around 0.4.5 +@app.route("/weighted-settings") def weighted_settings(): - return render_template(f"weighted-settings.html") + return redirect("weighted-options", 301) -# Player settings pages -@app.route('/games//player-settings') +@app.route("/weighted-options") @cache.cached() -def player_settings(game): - return render_template(f"player-settings.html", game=game, theme=get_world_theme(game)) +def weighted_options(): + return render_template("weighted-options.html") + + +# TODO for back compat. remove around 0.4.5 +@app.route("/games//player-settings") +def player_settings(game: str): + return redirect(url_for("player_options", game=game), 301) + + +# Player options pages +@app.route("/games//player-options") +@cache.cached() +def player_options(game: str): + return render_template("player-options.html", game=game, theme=get_world_theme(game)) # Game Info Pages @@ -181,6 +193,6 @@ def get_sitemap(): available_games: List[Dict[str, Union[str, bool]]] = [] for game, world in AutoWorldRegister.world_types.items(): if not world.hidden: - has_settings: bool = isinstance(world.web.settings_page, bool) and world.web.settings_page + has_settings: bool = isinstance(world.web.options_page, bool) and world.web.options_page available_games.append({ 'title': game, 'has_settings': has_settings }) return render_template("siteMap.html", games=available_games) diff --git a/WebHostLib/options.py b/WebHostLib/options.py index 18a28045ee..785785cde0 100644 --- a/WebHostLib/options.py +++ b/WebHostLib/options.py @@ -25,7 +25,7 @@ def create(): return "Please document me!" return "\n".join(line.strip() for line in option_type.__doc__.split("\n")).strip() - weighted_settings = { + weighted_options = { "baseOptions": { "description": "Generated by https://archipelago.gg/", "name": "Player", @@ -38,8 +38,8 @@ def create(): all_options: typing.Dict[str, Options.AssembleOptions] = world.options_dataclass.type_hints - # Generate JSON files for player-settings pages - player_settings = { + # Generate JSON files for player-options pages + player_options = { "baseOptions": { "description": f"Generated by https://archipelago.gg/ for {game_name}", "game": game_name, @@ -117,17 +117,17 @@ def create(): } else: - logging.debug(f"{option} not exported to Web Settings.") + logging.debug(f"{option} not exported to Web options.") - player_settings["gameOptions"] = game_options + player_options["gameOptions"] = game_options - os.makedirs(os.path.join(target_folder, 'player-settings'), exist_ok=True) + os.makedirs(os.path.join(target_folder, 'player-options'), exist_ok=True) - with open(os.path.join(target_folder, 'player-settings', game_name + ".json"), "w") as f: - json.dump(player_settings, f, indent=2, separators=(',', ': ')) + with open(os.path.join(target_folder, 'player-options', game_name + ".json"), "w") as f: + json.dump(player_options, f, indent=2, separators=(',', ': ')) - if not world.hidden and world.web.settings_page is True: - # Add the random option to Choice, TextChoice, and Toggle settings + if not world.hidden and world.web.options_page is True: + # Add the random option to Choice, TextChoice, and Toggle options for option in game_options.values(): if option["type"] == "select": option["options"].append({"name": "Random", "value": "random"}) @@ -135,11 +135,11 @@ def create(): if not option["defaultValue"]: option["defaultValue"] = "random" - weighted_settings["baseOptions"]["game"][game_name] = 0 - weighted_settings["games"][game_name] = {} - weighted_settings["games"][game_name]["gameSettings"] = game_options - weighted_settings["games"][game_name]["gameItems"] = tuple(world.item_names) - weighted_settings["games"][game_name]["gameLocations"] = tuple(world.location_names) + weighted_options["baseOptions"]["game"][game_name] = 0 + weighted_options["games"][game_name] = {} + weighted_options["games"][game_name]["gameSettings"] = game_options + weighted_options["games"][game_name]["gameItems"] = tuple(world.item_names) + weighted_options["games"][game_name]["gameLocations"] = tuple(world.location_names) - with open(os.path.join(target_folder, 'weighted-settings.json'), "w") as f: - json.dump(weighted_settings, f, indent=2, separators=(',', ': ')) + with open(os.path.join(target_folder, 'weighted-options.json'), "w") as f: + json.dump(weighted_options, f, indent=2, separators=(',', ': ')) diff --git a/WebHostLib/static/assets/player-settings.js b/WebHostLib/static/assets/player-options.js similarity index 63% rename from WebHostLib/static/assets/player-settings.js rename to WebHostLib/static/assets/player-options.js index 4ebec1adbf..727e0f63b9 100644 --- a/WebHostLib/static/assets/player-settings.js +++ b/WebHostLib/static/assets/player-options.js @@ -1,41 +1,41 @@ let gameName = null; window.addEventListener('load', () => { - gameName = document.getElementById('player-settings').getAttribute('data-game'); + gameName = document.getElementById('player-options').getAttribute('data-game'); // Update game name on page document.getElementById('game-name').innerText = gameName; - fetchSettingData().then((results) => { - let settingHash = localStorage.getItem(`${gameName}-hash`); - if (!settingHash) { + fetchOptionData().then((results) => { + let optionHash = localStorage.getItem(`${gameName}-hash`); + if (!optionHash) { // If no hash data has been set before, set it now - settingHash = md5(JSON.stringify(results)); - localStorage.setItem(`${gameName}-hash`, settingHash); + optionHash = md5(JSON.stringify(results)); + localStorage.setItem(`${gameName}-hash`, optionHash); localStorage.removeItem(gameName); } - if (settingHash !== md5(JSON.stringify(results))) { - showUserMessage("Your settings are out of date! Click here to update them! Be aware this will reset " + + if (optionHash !== md5(JSON.stringify(results))) { + showUserMessage("Your options are out of date! Click here to update them! Be aware this will reset " + "them all to default."); - document.getElementById('user-message').addEventListener('click', resetSettings); + document.getElementById('user-message').addEventListener('click', resetOptions); } // Page setup - createDefaultSettings(results); + createDefaultOptions(results); buildUI(results); adjustHeaderWidth(); // Event listeners - document.getElementById('export-settings').addEventListener('click', () => exportSettings()); + document.getElementById('export-options').addEventListener('click', () => exportOptions()); document.getElementById('generate-race').addEventListener('click', () => generateGame(true)); document.getElementById('generate-game').addEventListener('click', () => generateGame()); // Name input field - const playerSettings = JSON.parse(localStorage.getItem(gameName)); + const playerOptions = JSON.parse(localStorage.getItem(gameName)); const nameInput = document.getElementById('player-name'); - nameInput.addEventListener('keyup', (event) => updateBaseSetting(event)); - nameInput.value = playerSettings.name; + nameInput.addEventListener('keyup', (event) => updateBaseOption(event)); + nameInput.value = playerOptions.name; }).catch((e) => { console.error(e); const url = new URL(window.location.href); @@ -43,13 +43,13 @@ window.addEventListener('load', () => { }) }); -const resetSettings = () => { +const resetOptions = () => { localStorage.removeItem(gameName); localStorage.removeItem(`${gameName}-hash`) window.location.reload(); }; -const fetchSettingData = () => new Promise((resolve, reject) => { +const fetchOptionData = () => new Promise((resolve, reject) => { const ajax = new XMLHttpRequest(); ajax.onreadystatechange = () => { if (ajax.readyState !== 4) { return; } @@ -60,54 +60,54 @@ const fetchSettingData = () => new Promise((resolve, reject) => { try{ resolve(JSON.parse(ajax.responseText)); } catch(error){ reject(error); } }; - ajax.open('GET', `${window.location.origin}/static/generated/player-settings/${gameName}.json`, true); + ajax.open('GET', `${window.location.origin}/static/generated/player-options/${gameName}.json`, true); ajax.send(); }); -const createDefaultSettings = (settingData) => { +const createDefaultOptions = (optionData) => { if (!localStorage.getItem(gameName)) { - const newSettings = { + const newOptions = { [gameName]: {}, }; - for (let baseOption of Object.keys(settingData.baseOptions)){ - newSettings[baseOption] = settingData.baseOptions[baseOption]; + for (let baseOption of Object.keys(optionData.baseOptions)){ + newOptions[baseOption] = optionData.baseOptions[baseOption]; } - for (let gameOption of Object.keys(settingData.gameOptions)){ - newSettings[gameName][gameOption] = settingData.gameOptions[gameOption].defaultValue; + for (let gameOption of Object.keys(optionData.gameOptions)){ + newOptions[gameName][gameOption] = optionData.gameOptions[gameOption].defaultValue; } - localStorage.setItem(gameName, JSON.stringify(newSettings)); + localStorage.setItem(gameName, JSON.stringify(newOptions)); } }; -const buildUI = (settingData) => { +const buildUI = (optionData) => { // Game Options const leftGameOpts = {}; const rightGameOpts = {}; - Object.keys(settingData.gameOptions).forEach((key, index) => { - if (index < Object.keys(settingData.gameOptions).length / 2) { leftGameOpts[key] = settingData.gameOptions[key]; } - else { rightGameOpts[key] = settingData.gameOptions[key]; } + Object.keys(optionData.gameOptions).forEach((key, index) => { + if (index < Object.keys(optionData.gameOptions).length / 2) { leftGameOpts[key] = optionData.gameOptions[key]; } + else { rightGameOpts[key] = optionData.gameOptions[key]; } }); document.getElementById('game-options-left').appendChild(buildOptionsTable(leftGameOpts)); document.getElementById('game-options-right').appendChild(buildOptionsTable(rightGameOpts)); }; -const buildOptionsTable = (settings, romOpts = false) => { - const currentSettings = JSON.parse(localStorage.getItem(gameName)); +const buildOptionsTable = (options, romOpts = false) => { + const currentOptions = JSON.parse(localStorage.getItem(gameName)); const table = document.createElement('table'); const tbody = document.createElement('tbody'); - Object.keys(settings).forEach((setting) => { + Object.keys(options).forEach((option) => { const tr = document.createElement('tr'); // td Left const tdl = document.createElement('td'); const label = document.createElement('label'); - label.textContent = `${settings[setting].displayName}: `; - label.setAttribute('for', setting); + label.textContent = `${options[option].displayName}: `; + label.setAttribute('for', option); const questionSpan = document.createElement('span'); questionSpan.classList.add('interactive'); - questionSpan.setAttribute('data-tooltip', settings[setting].description); + questionSpan.setAttribute('data-tooltip', options[option].description); questionSpan.innerText = '(?)'; label.appendChild(questionSpan); @@ -120,36 +120,36 @@ const buildOptionsTable = (settings, romOpts = false) => { const randomButton = document.createElement('button'); - switch(settings[setting].type){ + switch(options[option].type){ case 'select': element = document.createElement('div'); element.classList.add('select-container'); let select = document.createElement('select'); - select.setAttribute('id', setting); - select.setAttribute('data-key', setting); + select.setAttribute('id', option); + select.setAttribute('data-key', option); if (romOpts) { select.setAttribute('data-romOpt', '1'); } - settings[setting].options.forEach((opt) => { + options[option].options.forEach((opt) => { const option = document.createElement('option'); option.setAttribute('value', opt.value); option.innerText = opt.name; - if ((isNaN(currentSettings[gameName][setting]) && - (parseInt(opt.value, 10) === parseInt(currentSettings[gameName][setting]))) || - (opt.value === currentSettings[gameName][setting])) + if ((isNaN(currentOptions[gameName][option]) && + (parseInt(opt.value, 10) === parseInt(currentOptions[gameName][option]))) || + (opt.value === currentOptions[gameName][option])) { option.selected = true; } select.appendChild(option); }); - select.addEventListener('change', (event) => updateGameSetting(event.target)); + select.addEventListener('change', (event) => updateGameOption(event.target)); element.appendChild(select); // Randomize button randomButton.innerText = '🎲'; randomButton.classList.add('randomize-button'); - randomButton.setAttribute('data-key', setting); + randomButton.setAttribute('data-key', option); randomButton.setAttribute('data-tooltip', 'Toggle randomization for this option!'); randomButton.addEventListener('click', (event) => toggleRandomize(event, select)); - if (currentSettings[gameName][setting] === 'random') { + if (currentOptions[gameName][option] === 'random') { randomButton.classList.add('active'); select.disabled = true; } @@ -163,30 +163,30 @@ const buildOptionsTable = (settings, romOpts = false) => { let range = document.createElement('input'); range.setAttribute('type', 'range'); - range.setAttribute('data-key', setting); - range.setAttribute('min', settings[setting].min); - range.setAttribute('max', settings[setting].max); - range.value = currentSettings[gameName][setting]; + range.setAttribute('data-key', option); + range.setAttribute('min', options[option].min); + range.setAttribute('max', options[option].max); + range.value = currentOptions[gameName][option]; range.addEventListener('change', (event) => { - document.getElementById(`${setting}-value`).innerText = event.target.value; - updateGameSetting(event.target); + document.getElementById(`${option}-value`).innerText = event.target.value; + updateGameOption(event.target); }); element.appendChild(range); let rangeVal = document.createElement('span'); rangeVal.classList.add('range-value'); - rangeVal.setAttribute('id', `${setting}-value`); - rangeVal.innerText = currentSettings[gameName][setting] !== 'random' ? - currentSettings[gameName][setting] : settings[setting].defaultValue; + rangeVal.setAttribute('id', `${option}-value`); + rangeVal.innerText = currentOptions[gameName][option] !== 'random' ? + currentOptions[gameName][option] : options[option].defaultValue; element.appendChild(rangeVal); // Randomize button randomButton.innerText = '🎲'; randomButton.classList.add('randomize-button'); - randomButton.setAttribute('data-key', setting); + randomButton.setAttribute('data-key', option); randomButton.setAttribute('data-tooltip', 'Toggle randomization for this option!'); randomButton.addEventListener('click', (event) => toggleRandomize(event, range)); - if (currentSettings[gameName][setting] === 'random') { + if (currentOptions[gameName][option] === 'random') { randomButton.classList.add('active'); range.disabled = true; } @@ -200,11 +200,11 @@ const buildOptionsTable = (settings, romOpts = false) => { // Build the select element let specialRangeSelect = document.createElement('select'); - specialRangeSelect.setAttribute('data-key', setting); - Object.keys(settings[setting].value_names).forEach((presetName) => { + specialRangeSelect.setAttribute('data-key', option); + Object.keys(options[option].value_names).forEach((presetName) => { let presetOption = document.createElement('option'); presetOption.innerText = presetName; - presetOption.value = settings[setting].value_names[presetName]; + presetOption.value = options[option].value_names[presetName]; const words = presetOption.innerText.split("_"); for (let i = 0; i < words.length; i++) { words[i] = words[i][0].toUpperCase() + words[i].substring(1); @@ -217,8 +217,8 @@ const buildOptionsTable = (settings, romOpts = false) => { customOption.value = 'custom'; customOption.selected = true; specialRangeSelect.appendChild(customOption); - if (Object.values(settings[setting].value_names).includes(Number(currentSettings[gameName][setting]))) { - specialRangeSelect.value = Number(currentSettings[gameName][setting]); + if (Object.values(options[option].value_names).includes(Number(currentOptions[gameName][option]))) { + specialRangeSelect.value = Number(currentOptions[gameName][option]); } // Build range element @@ -226,17 +226,17 @@ const buildOptionsTable = (settings, romOpts = false) => { specialRangeWrapper.classList.add('special-range-wrapper'); let specialRange = document.createElement('input'); specialRange.setAttribute('type', 'range'); - specialRange.setAttribute('data-key', setting); - specialRange.setAttribute('min', settings[setting].min); - specialRange.setAttribute('max', settings[setting].max); - specialRange.value = currentSettings[gameName][setting]; + specialRange.setAttribute('data-key', option); + specialRange.setAttribute('min', options[option].min); + specialRange.setAttribute('max', options[option].max); + specialRange.value = currentOptions[gameName][option]; // Build rage value element let specialRangeVal = document.createElement('span'); specialRangeVal.classList.add('range-value'); - specialRangeVal.setAttribute('id', `${setting}-value`); - specialRangeVal.innerText = currentSettings[gameName][setting] !== 'random' ? - currentSettings[gameName][setting] : settings[setting].defaultValue; + specialRangeVal.setAttribute('id', `${option}-value`); + specialRangeVal.innerText = currentOptions[gameName][option] !== 'random' ? + currentOptions[gameName][option] : options[option].defaultValue; // Configure select event listener specialRangeSelect.addEventListener('change', (event) => { @@ -244,18 +244,18 @@ const buildOptionsTable = (settings, romOpts = false) => { // Update range slider specialRange.value = event.target.value; - document.getElementById(`${setting}-value`).innerText = event.target.value; - updateGameSetting(event.target); + document.getElementById(`${option}-value`).innerText = event.target.value; + updateGameOption(event.target); }); // Configure range event handler specialRange.addEventListener('change', (event) => { // Update select element specialRangeSelect.value = - (Object.values(settings[setting].value_names).includes(parseInt(event.target.value))) ? + (Object.values(options[option].value_names).includes(parseInt(event.target.value))) ? parseInt(event.target.value) : 'custom'; - document.getElementById(`${setting}-value`).innerText = event.target.value; - updateGameSetting(event.target); + document.getElementById(`${option}-value`).innerText = event.target.value; + updateGameOption(event.target); }); element.appendChild(specialRangeSelect); @@ -266,12 +266,12 @@ const buildOptionsTable = (settings, romOpts = false) => { // Randomize button randomButton.innerText = '🎲'; randomButton.classList.add('randomize-button'); - randomButton.setAttribute('data-key', setting); + randomButton.setAttribute('data-key', option); randomButton.setAttribute('data-tooltip', 'Toggle randomization for this option!'); randomButton.addEventListener('click', (event) => toggleRandomize( event, specialRange, specialRangeSelect) ); - if (currentSettings[gameName][setting] === 'random') { + if (currentOptions[gameName][option] === 'random') { randomButton.classList.add('active'); specialRange.disabled = true; specialRangeSelect.disabled = true; @@ -281,7 +281,7 @@ const buildOptionsTable = (settings, romOpts = false) => { break; default: - console.error(`Ignoring unknown setting type: ${settings[setting].type} with name ${setting}`); + console.error(`Ignoring unknown option type: ${options[option].type} with name ${option}`); return; } @@ -311,35 +311,35 @@ const toggleRandomize = (event, inputElement, optionalSelectElement = null) => { optionalSelectElement.disabled = true; } } - updateGameSetting(active ? inputElement : randomButton); + updateGameOption(active ? inputElement : randomButton); }; -const updateBaseSetting = (event) => { +const updateBaseOption = (event) => { const options = JSON.parse(localStorage.getItem(gameName)); options[event.target.getAttribute('data-key')] = isNaN(event.target.value) ? event.target.value : parseInt(event.target.value); localStorage.setItem(gameName, JSON.stringify(options)); }; -const updateGameSetting = (settingElement) => { +const updateGameOption = (optionElement) => { const options = JSON.parse(localStorage.getItem(gameName)); - if (settingElement.classList.contains('randomize-button')) { + if (optionElement.classList.contains('randomize-button')) { // If the event passed in is the randomize button, then we know what we must do. - options[gameName][settingElement.getAttribute('data-key')] = 'random'; + options[gameName][optionElement.getAttribute('data-key')] = 'random'; } else { - options[gameName][settingElement.getAttribute('data-key')] = isNaN(settingElement.value) ? - settingElement.value : parseInt(settingElement.value, 10); + options[gameName][optionElement.getAttribute('data-key')] = isNaN(optionElement.value) ? + optionElement.value : parseInt(optionElement.value, 10); } localStorage.setItem(gameName, JSON.stringify(options)); }; -const exportSettings = () => { - const settings = JSON.parse(localStorage.getItem(gameName)); - if (!settings.name || settings.name.toLowerCase() === 'player' || settings.name.trim().length === 0) { +const exportOptions = () => { + const options = JSON.parse(localStorage.getItem(gameName)); + if (!options.name || options.name.toLowerCase() === 'player' || options.name.trim().length === 0) { return showUserMessage('You must enter a player name!'); } - const yamlText = jsyaml.safeDump(settings, { noCompatMode: true }).replaceAll(/'(\d+)':/g, (x, y) => `${y}:`); + const yamlText = jsyaml.safeDump(options, { noCompatMode: true }).replaceAll(/'(\d+)':/g, (x, y) => `${y}:`); download(`${document.getElementById('player-name').value}.yaml`, yamlText); }; @@ -355,14 +355,14 @@ const download = (filename, text) => { }; const generateGame = (raceMode = false) => { - const settings = JSON.parse(localStorage.getItem(gameName)); - if (!settings.name || settings.name.toLowerCase() === 'player' || settings.name.trim().length === 0) { + const options = JSON.parse(localStorage.getItem(gameName)); + if (!options.name || options.name.toLowerCase() === 'player' || options.name.trim().length === 0) { return showUserMessage('You must enter a player name!'); } axios.post('/api/generate', { - weights: { player: settings }, - presetData: { player: settings }, + weights: { player: options }, + presetData: { player: options }, playerCount: 1, spoiler: 3, race: raceMode ? '1' : '0', diff --git a/WebHostLib/static/assets/weighted-settings.js b/WebHostLib/static/assets/weighted-options.js similarity index 99% rename from WebHostLib/static/assets/weighted-settings.js rename to WebHostLib/static/assets/weighted-options.js index 2cd61d2e6e..bdd121eff5 100644 --- a/WebHostLib/static/assets/weighted-settings.js +++ b/WebHostLib/static/assets/weighted-options.js @@ -23,7 +23,7 @@ window.addEventListener('load', () => { adjustHeaderWidth(); // Event listeners - document.getElementById('export-settings').addEventListener('click', () => settings.export()); + document.getElementById('export-options').addEventListener('click', () => settings.export()); document.getElementById('generate-race').addEventListener('click', () => settings.generateGame(true)); document.getElementById('generate-game').addEventListener('click', () => settings.generateGame()); diff --git a/WebHostLib/static/styles/player-settings.css b/WebHostLib/static/styles/player-options.css similarity index 67% rename from WebHostLib/static/styles/player-settings.css rename to WebHostLib/static/styles/player-options.css index e6e0c29292..2f5481d285 100644 --- a/WebHostLib/static/styles/player-settings.css +++ b/WebHostLib/static/styles/player-options.css @@ -4,7 +4,7 @@ html{ background-size: 650px 650px; } -#player-settings{ +#player-options{ box-sizing: border-box; max-width: 1024px; margin-left: auto; @@ -15,14 +15,14 @@ html{ color: #eeffeb; } -#player-settings #player-settings-button-row{ +#player-options #player-options-button-row{ display: flex; flex-direction: row; justify-content: space-between; margin-top: 15px; } -#player-settings code{ +#player-options code{ background-color: #d9cd8e; border-radius: 4px; padding-left: 0.25rem; @@ -30,7 +30,7 @@ html{ color: #000000; } -#player-settings #user-message{ +#player-options #user-message{ display: none; width: calc(100% - 8px); background-color: #ffe86b; @@ -40,12 +40,12 @@ html{ text-align: center; } -#player-settings #user-message.visible{ +#player-options #user-message.visible{ display: block; cursor: pointer; } -#player-settings h1{ +#player-options h1{ font-size: 2.5rem; font-weight: normal; width: 100%; @@ -53,7 +53,7 @@ html{ text-shadow: 1px 1px 4px #000000; } -#player-settings h2{ +#player-options h2{ font-size: 40px; font-weight: normal; width: 100%; @@ -62,22 +62,22 @@ html{ text-shadow: 1px 1px 2px #000000; } -#player-settings h3, #player-settings h4, #player-settings h5, #player-settings h6{ +#player-options h3, #player-options h4, #player-options h5, #player-options h6{ text-shadow: 1px 1px 1px rgba(0, 0, 0, 0.5); } -#player-settings input:not([type]){ +#player-options input:not([type]){ border: 1px solid #000000; padding: 3px; border-radius: 3px; min-width: 150px; } -#player-settings input:not([type]):focus{ +#player-options input:not([type]):focus{ border: 1px solid #ffffff; } -#player-settings select{ +#player-options select{ border: 1px solid #000000; padding: 3px; border-radius: 3px; @@ -85,72 +85,72 @@ html{ background-color: #ffffff; } -#player-settings #game-options, #player-settings #rom-options{ +#player-options #game-options, #player-options #rom-options{ display: flex; flex-direction: row; } -#player-settings .left, #player-settings .right{ +#player-options .left, #player-options .right{ flex-grow: 1; } -#player-settings .left{ +#player-options .left{ margin-right: 10px; } -#player-settings .right{ +#player-options .right{ margin-left: 10px; } -#player-settings table{ +#player-options table{ margin-bottom: 30px; width: 100%; } -#player-settings table .select-container{ +#player-options table .select-container{ display: flex; flex-direction: row; } -#player-settings table .select-container select{ +#player-options table .select-container select{ min-width: 200px; flex-grow: 1; } -#player-settings table select:disabled{ +#player-options table select:disabled{ background-color: lightgray; } -#player-settings table .range-container{ +#player-options table .range-container{ display: flex; flex-direction: row; } -#player-settings table .range-container input[type=range]{ +#player-options table .range-container input[type=range]{ flex-grow: 1; } -#player-settings table .range-value{ +#player-options table .range-value{ min-width: 20px; margin-left: 0.25rem; } -#player-settings table .special-range-container{ +#player-options table .special-range-container{ display: flex; flex-direction: column; } -#player-settings table .special-range-wrapper{ +#player-options table .special-range-wrapper{ display: flex; flex-direction: row; margin-top: 0.25rem; } -#player-settings table .special-range-wrapper input[type=range]{ +#player-options table .special-range-wrapper input[type=range]{ flex-grow: 1; } -#player-settings table .randomize-button { +#player-options table .randomize-button { max-height: 24px; line-height: 16px; padding: 2px 8px; @@ -160,23 +160,23 @@ html{ border-radius: 3px; } -#player-settings table .randomize-button.active { +#player-options table .randomize-button.active { background-color: #ffef00; /* Same as .interactive in globalStyles.css */ } -#player-settings table .randomize-button[data-tooltip]::after { +#player-options table .randomize-button[data-tooltip]::after { left: unset; right: 0; } -#player-settings table label{ +#player-options table label{ display: block; min-width: 200px; margin-right: 4px; cursor: default; } -#player-settings th, #player-settings td{ +#player-options th, #player-options td{ border: none; padding: 3px; font-size: 17px; @@ -184,17 +184,17 @@ html{ } @media all and (max-width: 1024px) { - #player-settings { + #player-options { border-radius: 0; } - #player-settings #game-options{ + #player-options #game-options{ justify-content: flex-start; flex-wrap: wrap; } - #player-settings .left, - #player-settings .right { + #player-options .left, + #player-options .right { margin: 0; } diff --git a/WebHostLib/static/styles/weighted-settings.css b/WebHostLib/static/styles/weighted-options.css similarity index 100% rename from WebHostLib/static/styles/weighted-settings.css rename to WebHostLib/static/styles/weighted-options.css diff --git a/WebHostLib/templates/player-settings.html b/WebHostLib/templates/player-options.html similarity index 75% rename from WebHostLib/templates/player-settings.html rename to WebHostLib/templates/player-options.html index 50b9e3cbb1..701b4e5861 100644 --- a/WebHostLib/templates/player-settings.html +++ b/WebHostLib/templates/player-options.html @@ -1,26 +1,26 @@ {% extends 'pageWrapper.html' %} {% block head %} - {{ game }} Settings + {{ game }} Options - + - + {% endblock %} {% block body %} {% include 'header/'+theme+'Header.html' %} -
+ -
- +
+
diff --git a/WebHostLib/templates/siteMap.html b/WebHostLib/templates/siteMap.html index 562dd3b71b..231ec83e24 100644 --- a/WebHostLib/templates/siteMap.html +++ b/WebHostLib/templates/siteMap.html @@ -24,7 +24,7 @@
  • Supported Games Page
  • Tutorials Page
  • User Content
  • -
  • Weighted Settings Page
  • +
  • Weighted Options Page
  • Game Statistics
  • Glossary
  • @@ -46,11 +46,11 @@ {% endfor %} -

    Game Settings Pages

    +

    Game Options Pages

    diff --git a/WebHostLib/templates/supportedGames.html b/WebHostLib/templates/supportedGames.html index f1514d8353..3252b16ad4 100644 --- a/WebHostLib/templates/supportedGames.html +++ b/WebHostLib/templates/supportedGames.html @@ -51,12 +51,12 @@ | Setup Guides {% endif %} - {% if world.web.settings_page is string %} + {% if world.web.options_page is string %} | - Settings Page - {% elif world.web.settings_page %} + Options Page + {% elif world.web.options_page %} | - Settings Page + Options Page {% endif %} {% if world.web.bug_report_page %} | diff --git a/WebHostLib/templates/weighted-settings.html b/WebHostLib/templates/weighted-options.html similarity index 82% rename from WebHostLib/templates/weighted-settings.html rename to WebHostLib/templates/weighted-options.html index 9ce097c37f..032a4eeb90 100644 --- a/WebHostLib/templates/weighted-settings.html +++ b/WebHostLib/templates/weighted-options.html @@ -1,26 +1,26 @@ {% extends 'pageWrapper.html' %} {% block head %} - {{ game }} Settings + {{ game }} Options - + - + {% endblock %} {% block body %} {% include 'header/grassHeader.html' %}
    -

    Weighted Settings

    -

    Weighted Settings allows you to choose how likely a particular option is to be used in game generation. +

    Weighted Options

    +

    Weighted options allow you to choose how likely a particular option is to be used in game generation. The higher an option is weighted, the more likely the option will be chosen. Think of them like entries in a raffle.

    Choose the games and options you would like to play with! You may generate a single-player game from - this page, or download a settings file you can use to participate in a MultiWorld.

    + this page, or download an options file you can use to participate in a MultiWorld.

    A list of all games you have generated can be found on the User Content page.

    @@ -40,7 +40,7 @@
    - +
    diff --git a/worlds/AutoWorld.py b/worlds/AutoWorld.py index 9a8b6a56ef..d4fe0f49a2 100644 --- a/worlds/AutoWorld.py +++ b/worlds/AutoWorld.py @@ -149,7 +149,7 @@ def call_stage(multiworld: "MultiWorld", method_name: str, *args: Any) -> None: class WebWorld: """Webhost integration""" - settings_page: Union[bool, str] = True + options_page: Union[bool, str] = True """display a settings page. Can be a link to a specific page or external tool.""" game_info_languages: List[str] = ['en'] diff --git a/worlds/bk_sudoku/__init__.py b/worlds/bk_sudoku/__init__.py index f914baf066..36d863bb44 100644 --- a/worlds/bk_sudoku/__init__.py +++ b/worlds/bk_sudoku/__init__.py @@ -5,7 +5,7 @@ from ..AutoWorld import WebWorld, World class Bk_SudokuWebWorld(WebWorld): - settings_page = "games/Sudoku/info/en" + options_page = "games/Sudoku/info/en" theme = 'partyTime' tutorials = [ Tutorial( diff --git a/worlds/ff1/__init__.py b/worlds/ff1/__init__.py index 432467399e..16905cc6da 100644 --- a/worlds/ff1/__init__.py +++ b/worlds/ff1/__init__.py @@ -14,7 +14,7 @@ class FF1Settings(settings.Group): class FF1Web(WebWorld): - settings_page = "https://finalfantasyrandomizer.com/" + options_page = "https://finalfantasyrandomizer.com/" tutorials = [Tutorial( "Multiworld Setup Guide", "A guide to playing Final Fantasy multiworld. This guide only covers playing multiworld.", From 706a2b36db4b1f6fb5c759e320a0e505eedfd0bf Mon Sep 17 00:00:00 2001 From: Seldom <38388947+Seldom-SE@users.noreply.github.com> Date: Mon, 23 Oct 2023 22:27:57 -0700 Subject: [PATCH 36/54] Terraria Old One's Army tier 2 and 3 missing Hardmode req (#2342) --- worlds/terraria/Rules.dsv | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/worlds/terraria/Rules.dsv b/worlds/terraria/Rules.dsv index 5f5551e465..b511db54de 100644 --- a/worlds/terraria/Rules.dsv +++ b/worlds/terraria/Rules.dsv @@ -305,7 +305,7 @@ Hydraulic Volt Crusher; Calamity; Life Fruit; ; (@mech_boss(1) & Wall of Flesh) | (@calamity & (Living Shard | Wall of Flesh)); Get a Life; Achievement; Life Fruit; Topped Off; Achievement; Life Fruit; -Old One's Army Tier 2; Location | Item; #Old One's Army Tier 1 & (@mech_boss(1) | #Old One's Army Tier 3); +Old One's Army Tier 2; Location | Item; #Old One's Army Tier 1 & ((Wall of Flesh & @mech_boss(1)) | #Old One's Army Tier 3); // Brimstone Elemental Infernal Suevite; Calamity; @pickaxe(150) | Brimstone Elemental; @@ -410,7 +410,7 @@ Scoria Bar; Calamity; Seismic Hampick; Calamity | Pickaxe(210) | Hammer(95); Hardmode Anvil & Scoria Bar; Life Alloy; Calamity; (Hardmode Anvil & Cryonic Bar & Perennial Bar & Scoria Bar) | Necromantic Geode; Advanced Display; Calamity; Hardmode Anvil & Mysterious Circuitry & Dubious Plating & Life Alloy & Long Ranged Sensor Array; -Old One's Army Tier 3; Location | Item; #Old One's Army Tier 1 & Golem; +Old One's Army Tier 3; Location | Item; #Old One's Army Tier 1 & Wall of Flesh & Golem; // Martian Madness Martian Madness; Location | Item; Wall of Flesh & Golem; From 426e9d3090136f6803e1cdbb178acf794f75bd15 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Tue, 24 Oct 2023 08:16:46 +0200 Subject: [PATCH 37/54] LttP: make Triforce Piece progression_skip_balancing (#2351) --- worlds/alttp/Items.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worlds/alttp/Items.py b/worlds/alttp/Items.py index 40634de8da..18f96b2ddb 100644 --- a/worlds/alttp/Items.py +++ b/worlds/alttp/Items.py @@ -102,7 +102,7 @@ item_table = {'Bow': ItemData(IC.progression, None, 0x0B, 'You have\nchosen the\ 'Red Pendant': ItemData(IC.progression, 'Crystal', (0x01, 0x32, 0x60, 0x00, 0x69, 0x03), None, None, None, None, None, None, "the red pendant"), 'Triforce': ItemData(IC.progression, None, 0x6A, '\n YOU WIN!', 'and the triforce', 'victorious kid', 'victory for sale', 'fungus for the win', 'greedy boy wins game again', 'the Triforce'), 'Power Star': ItemData(IC.progression, None, 0x6B, 'a small victory', 'and the power star', 'star-struck kid', 'star for sale', 'see stars with shroom', 'mario powers up again', 'a Power Star'), - 'Triforce Piece': ItemData(IC.progression, None, 0x6C, 'a small victory', 'and the thirdforce', 'triangular kid', 'triangle for sale', 'fungus for triangle', 'wise boy has triangle again', 'a Triforce Piece'), + 'Triforce Piece': ItemData(IC.progression_skip_balancing, None, 0x6C, 'a small victory', 'and the thirdforce', 'triangular kid', 'triangle for sale', 'fungus for triangle', 'wise boy has triangle again', 'a Triforce Piece'), 'Crystal 1': ItemData(IC.progression, 'Crystal', (0x02, 0x34, 0x64, 0x40, 0x7F, 0x06), None, None, None, None, None, None, "a blue crystal"), 'Crystal 2': ItemData(IC.progression, 'Crystal', (0x10, 0x34, 0x64, 0x40, 0x79, 0x06), None, None, None, None, None, None, "a blue crystal"), 'Crystal 3': ItemData(IC.progression, 'Crystal', (0x40, 0x34, 0x64, 0x40, 0x6C, 0x06), None, None, None, None, None, None, "a blue crystal"), From 78a4b01db517eb9cd85e440b920d0725cfce704c Mon Sep 17 00:00:00 2001 From: black-sliver <59490463+black-sliver@users.noreply.github.com> Date: Tue, 24 Oct 2023 10:59:15 +0200 Subject: [PATCH 38/54] pytest: run tests on non-windows with new names (#2349) --- pytest.ini | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pytest.ini b/pytest.ini index 5599a3c90f..33e0bab8a9 100644 --- a/pytest.ini +++ b/pytest.ini @@ -1,4 +1,4 @@ [pytest] -python_files = Test*.py +python_files = test_*.py Test*.py # TODO: remove Test* once all worlds have been ported python_classes = Test -python_functions = test \ No newline at end of file +python_functions = test From 90c5f45a1f07a2dd954f36d7713fbbbd840bcfb5 Mon Sep 17 00:00:00 2001 From: Aaron Wagener Date: Tue, 24 Oct 2023 15:50:53 -0500 Subject: [PATCH 39/54] Options: have as_dict return set values as lists to reduce JSON footprint (#2354) * Options: return set values as lists to reduce JSON footprint * sorted() Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com> --------- Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com> --- Options.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Options.py b/Options.py index d9ddfc2e2f..9b4f9d9908 100644 --- a/Options.py +++ b/Options.py @@ -950,7 +950,10 @@ class CommonOptions(metaclass=OptionsMetaProperty): else: raise ValueError(f"{casing} is invalid casing for as_dict. " "Valid names are 'snake', 'camel', 'pascal', 'kebab'.") - option_results[display_name] = getattr(self, option_name).value + value = getattr(self, option_name).value + if isinstance(value, set): + value = sorted(value) + option_results[display_name] = value else: raise ValueError(f"{option_name} not found in {tuple(type(self).type_hints)}") return option_results From 58642edc17f447e18d9a8c04e3cf803b17b97355 Mon Sep 17 00:00:00 2001 From: black-sliver <59490463+black-sliver@users.noreply.github.com> Date: Sun, 8 Oct 2023 17:19:33 +0200 Subject: [PATCH 40/54] Core: allow multi-line and --hash in requirements.txt --- ModuleUpdate.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/ModuleUpdate.py b/ModuleUpdate.py index 209f2da672..c33e894e8b 100644 --- a/ModuleUpdate.py +++ b/ModuleUpdate.py @@ -67,14 +67,23 @@ def update(yes=False, force=False): install_pkg_resources(yes=yes) import pkg_resources + prev = "" # if a line ends in \ we store here and merge later for req_file in requirements_files: path = os.path.join(os.path.dirname(sys.argv[0]), req_file) if not os.path.exists(path): path = os.path.join(os.path.dirname(__file__), req_file) with open(path) as requirementsfile: for line in requirementsfile: - if not line or line[0] == "#": - continue # ignore comments + if not line or line.lstrip(" \t")[0] == "#": + if not prev: + continue # ignore comments + line = "" + elif line.rstrip("\r\n").endswith("\\"): + prev = prev + line.rstrip("\r\n")[:-1] + " " # continue on next line + continue + line = prev + line + line = line.split("--hash=")[0] # remove hashes from requirement for version checking + prev = "" if line.startswith(("https://", "git+https://")): # extract name and version for url rest = line.split('/')[-1] From e87d5d5ac2ac82f185c1d5c9ffc318dc917d2256 Mon Sep 17 00:00:00 2001 From: black-sliver <59490463+black-sliver@users.noreply.github.com> Date: Sun, 8 Oct 2023 17:27:05 +0200 Subject: [PATCH 41/54] SoE: update to v0.46.1 * install via pypi, pin hashes * add OoB logic option * add sequence break logic option * fix turd ball texts * add option to fix OoB * better textbox handling when turning in energy core fragments --- worlds/soe/Logic.py | 8 ++- worlds/soe/Options.py | 25 +++++++-- worlds/soe/__init__.py | 12 ++--- worlds/soe/requirements.txt | 54 ++++++++++++------- worlds/soe/test/__init__.py | 15 ++++++ .../test/{TestAccess.py => test_access.py} | 0 worlds/soe/test/{TestGoal.py => test_goal.py} | 0 worlds/soe/test/test_oob.py | 51 ++++++++++++++++++ worlds/soe/test/test_sequence_breaks.py | 45 ++++++++++++++++ 9 files changed, 180 insertions(+), 30 deletions(-) rename worlds/soe/test/{TestAccess.py => test_access.py} (100%) rename worlds/soe/test/{TestGoal.py => test_goal.py} (100%) create mode 100644 worlds/soe/test/test_oob.py create mode 100644 worlds/soe/test/test_sequence_breaks.py diff --git a/worlds/soe/Logic.py b/worlds/soe/Logic.py index 3c173dec2f..e464b7fd3b 100644 --- a/worlds/soe/Logic.py +++ b/worlds/soe/Logic.py @@ -3,7 +3,7 @@ from typing import Protocol, Set from BaseClasses import MultiWorld from worlds.AutoWorld import LogicMixin from . import pyevermizer -from .Options import EnergyCore +from .Options import EnergyCore, OutOfBounds, SequenceBreaks # TODO: Options may preset certain progress steps (i.e. P_ROCK_SKIP), set in generate_early? @@ -61,4 +61,10 @@ class SecretOfEvermoreLogic(LogicMixin): if w.energy_core == EnergyCore.option_fragments: progress = pyevermizer.P_CORE_FRAGMENT count = w.required_fragments + elif progress == pyevermizer.P_ALLOW_OOB: + if world.worlds[player].out_of_bounds == OutOfBounds.option_logic: + return True + elif progress == pyevermizer.P_ALLOW_SEQUENCE_BREAKS: + if world.worlds[player].sequence_breaks == SequenceBreaks.option_logic: + return True return self._soe_count(progress, world, player, count) >= count diff --git a/worlds/soe/Options.py b/worlds/soe/Options.py index f1a30745f8..3de2de34ac 100644 --- a/worlds/soe/Options.py +++ b/worlds/soe/Options.py @@ -38,6 +38,12 @@ class OffOnFullChoice(Choice): alias_chaos = 2 +class OffOnLogicChoice(Choice): + option_off = 0 + option_on = 1 + option_logic = 2 + + # actual options class Difficulty(EvermizerFlags, Choice): """Changes relative spell cost and stuff""" @@ -93,10 +99,18 @@ class ExpModifier(Range): default = 200 -class FixSequence(EvermizerFlag, DefaultOnToggle): - """Fix some sequence breaks""" - display_name = "Fix Sequence" - flag = '1' +class SequenceBreaks(EvermizerFlags, OffOnLogicChoice): + """Disable, enable some sequence breaks or put them in logic""" + display_name = "Sequence Breaks" + default = 0 + flags = ['', 'j', 'J'] + + +class OutOfBounds(EvermizerFlags, OffOnLogicChoice): + """Disable, enable the out-of-bounds glitch or put it in logic""" + display_name = "Out Of Bounds" + default = 0 + flags = ['', 'u', 'U'] class FixCheats(EvermizerFlag, DefaultOnToggle): @@ -240,7 +254,8 @@ soe_options: typing.Dict[str, AssembleOptions] = { "available_fragments": AvailableFragments, "money_modifier": MoneyModifier, "exp_modifier": ExpModifier, - "fix_sequence": FixSequence, + "sequence_breaks": SequenceBreaks, + "out_of_bounds": OutOfBounds, "fix_cheats": FixCheats, "fix_infinite_ammo": FixInfiniteAmmo, "fix_atlas_glitch": FixAtlasGlitch, diff --git a/worlds/soe/__init__.py b/worlds/soe/__init__.py index f887325c60..4cc3c0866f 100644 --- a/worlds/soe/__init__.py +++ b/worlds/soe/__init__.py @@ -10,12 +10,8 @@ from worlds.generic.Rules import add_item_rule, set_rule from BaseClasses import Entrance, Item, ItemClassification, Location, LocationProgressType, Region, Tutorial from Utils import output_path -try: - import pyevermizer # from package -except ImportError: - import traceback - traceback.print_exc() - from . import pyevermizer # as part of the source tree +import pyevermizer # from package +# from . import pyevermizer # as part of the source tree from . import Logic # load logic mixin from .Options import soe_options, Difficulty, EnergyCore, RequiredFragments, AvailableFragments @@ -179,6 +175,8 @@ class SoEWorld(World): evermizer_seed: int connect_name: str energy_core: int + sequence_breaks: int + out_of_bounds: int available_fragments: int required_fragments: int @@ -191,6 +189,8 @@ class SoEWorld(World): def generate_early(self) -> None: # store option values that change logic self.energy_core = self.multiworld.energy_core[self.player].value + self.sequence_breaks = self.multiworld.sequence_breaks[self.player].value + self.out_of_bounds = self.multiworld.out_of_bounds[self.player].value self.required_fragments = self.multiworld.required_fragments[self.player].value if self.required_fragments > self.multiworld.available_fragments[self.player].value: self.multiworld.available_fragments[self.player].value = self.required_fragments diff --git a/worlds/soe/requirements.txt b/worlds/soe/requirements.txt index 878a2a80cc..710f51ddb0 100644 --- a/worlds/soe/requirements.txt +++ b/worlds/soe/requirements.txt @@ -1,18 +1,36 @@ -pyevermizer @ https://github.com/black-sliver/pyevermizer/releases/download/v0.44.0/pyevermizer-0.44.0-cp38-cp38-win_amd64.whl#0.44.0 ; sys_platform == 'win32' and platform_machine == 'AMD64' and python_version == '3.8' -pyevermizer @ https://github.com/black-sliver/pyevermizer/releases/download/v0.44.0/pyevermizer-0.44.0-cp39-cp39-win_amd64.whl#0.44.0 ; sys_platform == 'win32' and platform_machine == 'AMD64' and python_version == '3.9' -pyevermizer @ https://github.com/black-sliver/pyevermizer/releases/download/v0.44.0/pyevermizer-0.44.0-cp310-cp310-win_amd64.whl#0.44.0 ; sys_platform == 'win32' and platform_machine == 'AMD64' and python_version == '3.10' -pyevermizer @ https://github.com/black-sliver/pyevermizer/releases/download/v0.44.0/pyevermizer-0.44.0-cp311-cp311-win_amd64.whl#0.44.0 ; sys_platform == 'win32' and platform_machine == 'AMD64' and python_version == '3.11' -pyevermizer @ https://github.com/black-sliver/pyevermizer/releases/download/v0.44.0/pyevermizer-0.44.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl#0.44.0 ; sys_platform == 'linux' and platform_machine == 'x86_64' and python_version == '3.8' -pyevermizer @ https://github.com/black-sliver/pyevermizer/releases/download/v0.44.0/pyevermizer-0.44.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl#0.44.0 ; sys_platform == 'linux' and platform_machine == 'x86_64' and python_version == '3.9' -pyevermizer @ https://github.com/black-sliver/pyevermizer/releases/download/v0.44.0/pyevermizer-0.44.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl#0.44.0 ; sys_platform == 'linux' and platform_machine == 'x86_64' and python_version == '3.10' -pyevermizer @ https://github.com/black-sliver/pyevermizer/releases/download/v0.44.0/pyevermizer-0.44.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl#0.44.0 ; sys_platform == 'linux' and platform_machine == 'x86_64' and python_version == '3.11' -pyevermizer @ https://github.com/black-sliver/pyevermizer/releases/download/v0.44.0/pyevermizer-0.44.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl#0.44.0 ; sys_platform == 'linux' and platform_machine == 'aarch64' and python_version == '3.8' -pyevermizer @ https://github.com/black-sliver/pyevermizer/releases/download/v0.44.0/pyevermizer-0.44.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl#0.44.0 ; sys_platform == 'linux' and platform_machine == 'aarch64' and python_version == '3.9' -pyevermizer @ https://github.com/black-sliver/pyevermizer/releases/download/v0.44.0/pyevermizer-0.44.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl#0.44.0 ; sys_platform == 'linux' and platform_machine == 'aarch64' and python_version == '3.10' -pyevermizer @ https://github.com/black-sliver/pyevermizer/releases/download/v0.44.0/pyevermizer-0.44.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl#0.44.0 ; sys_platform == 'linux' and platform_machine == 'aarch64' and python_version == '3.11' -pyevermizer @ https://github.com/black-sliver/pyevermizer/releases/download/v0.44.0/pyevermizer-0.44.0-cp38-cp38-macosx_10_9_x86_64.whl#0.44.0 ; sys_platform == 'darwin' and python_version == '3.8' -#pyevermizer @ https://github.com/black-sliver/pyevermizer/releases/download/v0.44.0/pyevermizer-0.44.0-cp39-cp39-macosx_10_9_x86_64.whl#0.44.0 ; sys_platform == 'darwin' and python_version == '3.9' -pyevermizer @ https://github.com/black-sliver/pyevermizer/releases/download/v0.44.0/pyevermizer-0.44.0-cp39-cp39-macosx_10_9_universal2.whl#0.44.0 ; sys_platform == 'darwin' and python_version == '3.9' -pyevermizer @ https://github.com/black-sliver/pyevermizer/releases/download/v0.44.0/pyevermizer-0.44.0-cp310-cp310-macosx_10_9_universal2.whl#0.44.0 ; sys_platform == 'darwin' and python_version == '3.10' -pyevermizer @ https://github.com/black-sliver/pyevermizer/releases/download/v0.44.0/pyevermizer-0.44.0-cp311-cp311-macosx_10_9_universal2.whl#0.44.0 ; sys_platform == 'darwin' and python_version == '3.11' -pyevermizer @ https://github.com/black-sliver/pyevermizer/releases/download/v0.44.0/pyevermizer-0.44.0-1.tar.gz#0.44.0 ; python_version < '3.8' or python_version > '3.11' or (sys_platform != 'win32' and sys_platform != 'linux' and sys_platform != 'darwin') or (platform_machine != 'AMD64' and platform_machine != 'x86_64' and platform_machine != 'aarch64' and platform_machine != 'universal2' and platform_machine != 'arm64') +pyevermizer==0.46.1 \ + --hash=sha256:9fd71b5e4af26a5dd24a9cbf5320bf0111eef80320613401a1c03011b1515806 \ + --hash=sha256:23f553ed0509d9a238b2832f775e0b5abd7741b38ab60d388294ee8a7b96c5fb \ + --hash=sha256:7189b67766418a3e7e6c683f09c5e758aa1a5c24316dd9b714984bac099c4b75 \ + --hash=sha256:befa930711e63d5d5892f67fd888b2e65e746363e74599c53e71ecefb90ae16a \ + --hash=sha256:202933ce21e0f33859537bf3800d9a626c70262a9490962e3f450171758507ca \ + --hash=sha256:c20ca69311c696528e1122ebc7d33775ee971f538c0e3e05dd3bfd4de10b82d4 \ + --hash=sha256:74dc689a771ae5ffcd5257e763f571ee890e3e87bdb208233b7f451522c00d66 \ + --hash=sha256:072296baef464daeb6304cf58827dcbae441ad0803039aee1c0caa10d56e0674 \ + --hash=sha256:7921baf20d52d92d6aeb674125963c335b61abb7e1298bde4baf069d11a2d05e \ + --hash=sha256:ca098034a84007038c2bff004582e6e6ac2fa9cc8b9251301d25d7e2adcee6da \ + --hash=sha256:22ddb29823c19be9b15e1b3627db1babfe08b486aede7d5cc463a0a1ae4c75d8 \ + --hash=sha256:bf1c441b49026d9000166be6e2f63fc351a3fda170aa3fdf18d44d5e5d044640 \ + --hash=sha256:9710aa7957b4b1f14392006237eb95803acf27897377df3e85395f057f4316b9 \ + --hash=sha256:8feb676c198bee17ab991ee015828345ac3f87c27dfdb3061d92d1fe47c184b4 \ + --hash=sha256:597026dede72178ff3627a4eb3315de8444461c7f0f856f5773993c3f9790c53 \ + --hash=sha256:70f9b964bdfb5191e8f264644c5d1af3041c66fe15261df8a99b3d719dc680d6 \ + --hash=sha256:74655c0353ffb6cda30485091d0917ce703b128cd824b612b3110a85c79a93d0 \ + --hash=sha256:0e9c74d105d4ec3af12404e85bb8776931c043657add19f798ee69465f92b999 \ + --hash=sha256:d3c13446d3d482b9cce61ac73b38effd26fcdcf7f693a405868d3aaaa4d18ca6 \ + --hash=sha256:371ac3360640ef439a5920ddfe11a34e9d2e546ed886bb8c9ed312611f9f4655 \ + --hash=sha256:6e5cf63b036f24d2ae4375a88df8d0bc93208352939521d1fcac3c829ef2c363 \ + --hash=sha256:edf28f5c4d1950d17343adf6d8d40d12c7e982d1e39535d55f7915e122cd8b0e \ + --hash=sha256:b5ef6f3b4e04f677c296f60f7f4c320ac22cd5bc09c05574460116c8641c801a \ + --hash=sha256:dd651f66720af4abe2ddae29944e299a57ff91e6fca1739e6dc1f8fd7a8c2b39 \ + --hash=sha256:4e278f5f72c27f9703bce5514d2fead8c00361caac03e94b0bf9ad8a144f1eeb \ + --hash=sha256:38f36ea1f545b835c3ecd6e081685a233ac2e3cf0eec8916adc92e4d791098a6 \ + --hash=sha256:0a2e58ed6e7c42f006cc17d32cec1f432f01b3fe490e24d71471b36e0d0d8742 \ + --hash=sha256:c1b658db76240596c03571c60635abe953f36fb55b363202971831c2872ea9a0 \ + --hash=sha256:deb5a84a6a56325eb6701336cdbf70f72adaaeab33cbe953d0e551ecf2592f20 \ + --hash=sha256:b1425c793e0825f58b3726e7afebaf5a296c07cb0d28580d0ee93dbe10dcdf63 \ + --hash=sha256:11995fb4dfd14b5c359591baee2a864c5814650ba0084524d4ea0466edfaf029 \ + --hash=sha256:5d2120b5c93ae322fe2a85d48e3eab4168a19e974a880908f1ac291c0300940f \ + --hash=sha256:254912ea4bfaaffb0abe366e73bd9ecde622677d6afaf2ce8a0c330df99fefd9 \ + --hash=sha256:540d8e4525f0b5255c1554b4589089dc58e15df22f343e9545ea00f7012efa07 \ + --hash=sha256:f69b8ebded7eed181fabe30deabae89fd10c41964f38abb26b19664bbe55c1ae diff --git a/worlds/soe/test/__init__.py b/worlds/soe/test/__init__.py index 3c2a0dc1b6..27d38605aa 100644 --- a/worlds/soe/test/__init__.py +++ b/worlds/soe/test/__init__.py @@ -1,5 +1,20 @@ from test.TestBase import WorldTestBase +from typing import Iterable class SoETestBase(WorldTestBase): game = "Secret of Evermore" + + def assertLocationReachability(self, reachable: Iterable[str] = (), unreachable: Iterable[str] = (), + satisfied=True) -> None: + """ + Tests that unreachable can't be reached. Tests that reachable can be reached if satisfied=True. + Usage: test with satisfied=False, collect requirements into state, test again with satisfied=True + """ + for location in reachable: + self.assertEqual(self.can_reach_location(location), satisfied, + f"{location} is unreachable but should be" if satisfied else + f"{location} is reachable but shouldn't be") + for location in unreachable: + self.assertFalse(self.can_reach_location(location), + f"{location} is reachable but shouldn't be") diff --git a/worlds/soe/test/TestAccess.py b/worlds/soe/test/test_access.py similarity index 100% rename from worlds/soe/test/TestAccess.py rename to worlds/soe/test/test_access.py diff --git a/worlds/soe/test/TestGoal.py b/worlds/soe/test/test_goal.py similarity index 100% rename from worlds/soe/test/TestGoal.py rename to worlds/soe/test/test_goal.py diff --git a/worlds/soe/test/test_oob.py b/worlds/soe/test/test_oob.py new file mode 100644 index 0000000000..27e00cd3e7 --- /dev/null +++ b/worlds/soe/test/test_oob.py @@ -0,0 +1,51 @@ +import typing +from . import SoETestBase + + +class OoBTest(SoETestBase): + """Tests that 'on' doesn't put out-of-bounds in logic. This is also the test base for OoB in logic.""" + options: typing.Dict[str, typing.Any] = {"out_of_bounds": "on"} + + def testOoBAccess(self): + in_logic = self.options["out_of_bounds"] == "logic" + + # some locations that just need a weapon + OoB + oob_reachable = [ + "Aquagoth", "Sons of Sth.", "Mad Monk", "Magmar", # OoB can use volcano shop to skip rock skip + "Levitate", "Fireball", "Drain", "Speed", + "E. Crustacia #107", "Energy Core #285", "Vanilla Gauge #57", + ] + # some locations that should still be unreachable + oob_unreachable = [ + "Tiny", "Rimsala", + "Barrier", "Call Up", "Reflect", "Force Field", "Stop", # Stop guy doesn't spawn for the other entrances + "Pyramid bottom #118", "Tiny's hideout #160", "Tiny's hideout #161", "Greenhouse #275", + ] + # OoB + Diamond Eyes + de_reachable = [ + "Tiny's hideout #160", + ] + # still unreachable + de_unreachable = [ + "Tiny", + "Tiny's hideout #161", + ] + + self.assertLocationReachability(reachable=oob_reachable, unreachable=oob_unreachable, satisfied=False) + self.collect_by_name("Gladiator Sword") + self.assertLocationReachability(reachable=oob_reachable, unreachable=oob_unreachable, satisfied=in_logic) + self.collect_by_name("Diamond Eye") + self.assertLocationReachability(reachable=de_reachable, unreachable=de_unreachable, satisfied=in_logic) + + def testOoBGoal(self): + # still need Energy Core with OoB if sequence breaks are not in logic + for item in ["Gladiator Sword", "Diamond Eye", "Wheel", "Gauge"]: + self.collect_by_name(item) + self.assertBeatable(False) + self.collect_by_name("Energy Core") + self.assertBeatable(True) + + +class OoBInLogicTest(OoBTest): + """Tests that stuff that should be reachable/unreachable with out-of-bounds actually is.""" + options: typing.Dict[str, typing.Any] = {"out_of_bounds": "logic"} diff --git a/worlds/soe/test/test_sequence_breaks.py b/worlds/soe/test/test_sequence_breaks.py new file mode 100644 index 0000000000..4248f9b47d --- /dev/null +++ b/worlds/soe/test/test_sequence_breaks.py @@ -0,0 +1,45 @@ +import typing +from . import SoETestBase + + +class SequenceBreaksTest(SoETestBase): + """Tests that 'on' doesn't put sequence breaks in logic. This is also the test base for in-logic.""" + options: typing.Dict[str, typing.Any] = {"sequence_breaks": "on"} + + def testSequenceBreaksAccess(self): + in_logic = self.options["sequence_breaks"] == "logic" + + # some locations that just need any weapon + sequence break + break_reachable = [ + "Sons of Sth.", "Mad Monk", "Magmar", + "Fireball", + "Volcano Room1 #73", "Pyramid top #135", + ] + # some locations that should still be unreachable + break_unreachable = [ + "Aquagoth", "Megataur", "Tiny", "Rimsala", + "Barrier", "Call Up", "Levitate", "Stop", "Drain", "Escape", + "Greenhouse #275", "E. Crustacia #107", "Energy Core #285", "Vanilla Gauge #57", + ] + + self.assertLocationReachability(reachable=break_reachable, unreachable=break_unreachable, satisfied=False) + self.collect_by_name("Gladiator Sword") + self.assertLocationReachability(reachable=break_reachable, unreachable=break_unreachable, satisfied=in_logic) + self.collect_by_name("Spider Claw") # Gauge now just needs non-sword + self.assertEqual(self.can_reach_location("Vanilla Gauge #57"), in_logic) + self.collect_by_name("Bronze Spear") # Escape now just needs either Megataur or Rimsala dead + self.assertEqual(self.can_reach_location("Escape"), in_logic) + + def testSequenceBreaksGoal(self): + in_logic = self.options["sequence_breaks"] == "logic" + + # don't need Energy Core with sequence breaks in logic + for item in ["Gladiator Sword", "Diamond Eye", "Wheel", "Gauge"]: + self.assertBeatable(False) + self.collect_by_name(item) + self.assertBeatable(in_logic) + + +class SequenceBreaksInLogicTest(SequenceBreaksTest): + """Tests that stuff that should be reachable/unreachable with sequence breaks actually is.""" + options: typing.Dict[str, typing.Any] = {"sequence_breaks": "logic"} From e5554f8630b0b73e2b22ce33ab592ba70e9d9d6e Mon Sep 17 00:00:00 2001 From: black-sliver <59490463+black-sliver@users.noreply.github.com> Date: Wed, 25 Oct 2023 09:34:59 +0200 Subject: [PATCH 42/54] SoE: create regions cleanup and speedup (#2361) * SoE: create regions cleanup and speedup keep local reference instead of hitting multiworld cache also technically fixes a bug where all locations are in 'menu', not 'ingame' * SoE: somplify region connection --- worlds/soe/__init__.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/worlds/soe/__init__.py b/worlds/soe/__init__.py index 4cc3c0866f..9a8f38cdac 100644 --- a/worlds/soe/__init__.py +++ b/worlds/soe/__init__.py @@ -224,9 +224,8 @@ class SoEWorld(World): max_difficulty = 1 if self.multiworld.difficulty[self.player] == Difficulty.option_easy else 256 # TODO: generate *some* regions from locations' requirements? - r = Region('Menu', self.player, self.multiworld) - r.exits = [Entrance(self.player, 'New Game', r)] - self.multiworld.regions += [r] + menu = Region('Menu', self.player, self.multiworld) + self.multiworld.regions += [menu] def get_sphere_index(evermizer_loc): """Returns 0, 1 or 2 for locations in spheres 1, 2, 3+""" @@ -234,11 +233,14 @@ class SoEWorld(World): return 2 return min(2, len(evermizer_loc.requires)) + # create ingame region + ingame = Region('Ingame', self.player, self.multiworld) + # group locations into spheres (1, 2, 3+ at index 0, 1, 2) spheres: typing.Dict[int, typing.Dict[int, typing.List[SoELocation]]] = {} for loc in _locations: spheres.setdefault(get_sphere_index(loc), {}).setdefault(loc.type, []).append( - SoELocation(self.player, loc.name, self.location_name_to_id[loc.name], r, + SoELocation(self.player, loc.name, self.location_name_to_id[loc.name], ingame, loc.difficulty > max_difficulty)) # location balancing data @@ -280,18 +282,16 @@ class SoEWorld(World): late_locations = self.multiworld.random.sample(late_bosses, late_count) # add locations to the world - r = Region('Ingame', self.player, self.multiworld) for sphere in spheres.values(): for locations in sphere.values(): for location in locations: - r.locations.append(location) + ingame.locations.append(location) if location.name in late_locations: location.progress_type = LocationProgressType.PRIORITY - r.locations.append(SoELocation(self.player, 'Done', None, r)) - self.multiworld.regions += [r] - - self.multiworld.get_entrance('New Game', self.player).connect(self.multiworld.get_region('Ingame', self.player)) + ingame.locations.append(SoELocation(self.player, 'Done', None, ingame)) + menu.connect(ingame, "New Game") + self.multiworld.regions += [ingame] def create_items(self): # add regular items to the pool From be959c05a640363a1a57303ced968ca998b721d3 Mon Sep 17 00:00:00 2001 From: Aaron Wagener Date: Wed, 25 Oct 2023 02:56:56 -0500 Subject: [PATCH 43/54] The Messenger: speed up generation for large multiworlds (#2359) --- worlds/messenger/__init__.py | 6 +++--- worlds/messenger/subclasses.py | 1 - 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/worlds/messenger/__init__.py b/worlds/messenger/__init__.py index 4be699e9cf..0771989ffc 100644 --- a/worlds/messenger/__init__.py +++ b/worlds/messenger/__init__.py @@ -82,9 +82,7 @@ class MessengerWorld(World): self.shop_prices, self.figurine_prices = shuffle_shop_prices(self) def create_regions(self) -> None: - for region in [MessengerRegion(reg_name, self) for reg_name in REGIONS]: - if region.name in REGION_CONNECTIONS: - region.add_exits(REGION_CONNECTIONS[region.name]) + self.multiworld.regions += [MessengerRegion(reg_name, self) for reg_name in REGIONS] def create_items(self) -> None: # create items that are always in the item pool @@ -138,6 +136,8 @@ class MessengerWorld(World): self.multiworld.itempool += itempool def set_rules(self) -> None: + for reg_name, connections in REGION_CONNECTIONS.items(): + self.multiworld.get_region(reg_name, self.player).add_exits(connections) logic = self.options.logic_level if logic == Logic.option_normal: MessengerRules(self).set_messenger_rules() diff --git a/worlds/messenger/subclasses.py b/worlds/messenger/subclasses.py index c5d90e00c8..ce31d43d60 100644 --- a/worlds/messenger/subclasses.py +++ b/worlds/messenger/subclasses.py @@ -32,7 +32,6 @@ class MessengerRegion(Region): loc_dict = {loc: world.location_name_to_id[loc] if loc in world.location_name_to_id else None for loc in locations} self.add_locations(loc_dict, MessengerLocation) - world.multiworld.regions.append(self) class MessengerLocation(Location): From e5ca83b5dba69a6c6045bb3317e9f69c2d7af176 Mon Sep 17 00:00:00 2001 From: Felix R <50271878+FelicitusNeko@users.noreply.github.com> Date: Wed, 25 Oct 2023 05:22:09 -0300 Subject: [PATCH 44/54] Bumper Stickers: add location rules (#2254) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * bumpstik: treasure/booster location rules * bumpstik: oop missed a bit * bumpstik: apply access rule to Hazards check * bumpstik: move completion cond. to set_rules * bumpstik: tests? I have literally never written these before so 🤷 * bumpstik: oops * bumpstik: how about this? * bumpstik: fix some logic * bumpstik: this almost works but not quite * bumpstik: accurate region boundaries for BBs since we're using rules now * bumpstik: holy heck it works now --- worlds/bumpstik/Regions.py | 8 +++---- worlds/bumpstik/__init__.py | 19 ++++++++------- worlds/bumpstik/test/TestLogic.py | 39 +++++++++++++++++++++++++++++++ worlds/bumpstik/test/__init__.py | 5 ++++ 4 files changed, 59 insertions(+), 12 deletions(-) create mode 100644 worlds/bumpstik/test/TestLogic.py create mode 100644 worlds/bumpstik/test/__init__.py diff --git a/worlds/bumpstik/Regions.py b/worlds/bumpstik/Regions.py index 247d6d61a3..6cddde882a 100644 --- a/worlds/bumpstik/Regions.py +++ b/worlds/bumpstik/Regions.py @@ -23,13 +23,13 @@ def create_regions(world: MultiWorld, player: int): entrance_map = { "Level 1": lambda state: - state.has("Booster Bumper", player, 2) and state.has("Treasure Bumper", player, 9), + state.has("Booster Bumper", player, 1) and state.has("Treasure Bumper", player, 8), "Level 2": lambda state: - state.has("Booster Bumper", player, 3) and state.has("Treasure Bumper", player, 17), + state.has("Booster Bumper", player, 2) and state.has("Treasure Bumper", player, 16), "Level 3": lambda state: - state.has("Booster Bumper", player, 4) and state.has("Treasure Bumper", player, 25), + state.has("Booster Bumper", player, 3) and state.has("Treasure Bumper", player, 24), "Level 4": lambda state: - state.has("Booster Bumper", player, 5) and state.has("Treasure Bumper", player, 33) + state.has("Booster Bumper", player, 5) and state.has("Treasure Bumper", player, 32) } for x, region_name in enumerate(region_map): diff --git a/worlds/bumpstik/__init__.py b/worlds/bumpstik/__init__.py index 9eeb3325e3..c4e65d07b6 100644 --- a/worlds/bumpstik/__init__.py +++ b/worlds/bumpstik/__init__.py @@ -108,7 +108,7 @@ class BumpStikWorld(World): item_pool += self._create_item_in_quantities( name, frequencies[i]) - item_delta = len(location_table) - len(item_pool) - 1 + item_delta = len(location_table) - len(item_pool) if item_delta > 0: item_pool += self._create_item_in_quantities( "Score Bonus", item_delta) @@ -116,13 +116,16 @@ class BumpStikWorld(World): self.multiworld.itempool += item_pool def set_rules(self): - forbid_item(self.multiworld.get_location("Bonus Booster 5", self.player), - "Booster Bumper", self.player) - - def generate_basic(self): - self.multiworld.get_location("Level 5 - Cleared all Hazards", self.player).place_locked_item( - self.create_item(self.get_filler_item_name())) - + for x in range(1, 32): + self.multiworld.get_location(f"Treasure Bumper {x + 1}", self.player).access_rule = \ + lambda state, x = x: state.has("Treasure Bumper", self.player, x) + for x in range(1, 5): + self.multiworld.get_location(f"Bonus Booster {x + 1}", self.player).access_rule = \ + lambda state, x = x: state.has("Booster Bumper", self.player, x) + self.multiworld.get_location("Level 5 - Cleared all Hazards", self.player).access_rule = \ + lambda state: state.has("Hazard Bumper", self.player, 25) + self.multiworld.completion_condition[self.player] = \ lambda state: state.has("Booster Bumper", self.player, 5) and \ state.has("Treasure Bumper", self.player, 32) + diff --git a/worlds/bumpstik/test/TestLogic.py b/worlds/bumpstik/test/TestLogic.py new file mode 100644 index 0000000000..e374b7b1e9 --- /dev/null +++ b/worlds/bumpstik/test/TestLogic.py @@ -0,0 +1,39 @@ +from . import BumpStikTestBase + + +class TestRuleLogic(BumpStikTestBase): + def testLogic(self): + for x in range(1, 33): + if x == 32: + self.assertFalse(self.can_reach_location("Level 5 - Cleared all Hazards")) + + self.collect(self.get_item_by_name("Treasure Bumper")) + if x % 8 == 0: + bb_count = round(x / 8) + + if bb_count < 4: + self.assertFalse(self.can_reach_location(f"Treasure Bumper {x + 1}")) + elif bb_count == 4: + bb_count += 1 + + for y in range(self.count("Booster Bumper"), bb_count): + self.assertTrue(self.can_reach_location(f"Bonus Booster {y + 1}"), + f"BB {y + 1} check not reachable with {self.count('Booster Bumper')} BBs") + if y < 4: + self.assertFalse(self.can_reach_location(f"Bonus Booster {y + 2}"), + f"BB {y + 2} check reachable with {self.count('Treasure Bumper')} TBs") + self.collect(self.get_item_by_name("Booster Bumper")) + + if x < 31: + self.assertFalse(self.can_reach_location(f"Treasure Bumper {x + 2}")) + elif x == 31: + self.assertFalse(self.can_reach_location("Level 5 - 50,000+ Total Points")) + + if x < 32: + self.assertTrue(self.can_reach_location(f"Treasure Bumper {x + 1}"), + f"TB {x + 1} check not reachable with {self.count('Treasure Bumper')} TBs") + elif x == 32: + self.assertTrue(self.can_reach_location("Level 5 - 50,000+ Total Points")) + self.assertFalse(self.can_reach_location("Level 5 - Cleared all Hazards")) + self.collect(self.get_items_by_name("Hazard Bumper")) + self.assertTrue(self.can_reach_location("Level 5 - Cleared all Hazards")) diff --git a/worlds/bumpstik/test/__init__.py b/worlds/bumpstik/test/__init__.py new file mode 100644 index 0000000000..1199d7b8e5 --- /dev/null +++ b/worlds/bumpstik/test/__init__.py @@ -0,0 +1,5 @@ +from test.TestBase import WorldTestBase + + +class BumpStikTestBase(WorldTestBase): + game = "Bumper Stickers" From dab704df55dae017f1dbbdd6364f62d23b92ee60 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Wed, 25 Oct 2023 21:23:52 +0200 Subject: [PATCH 45/54] Core/LttP: remove initialize_regions (#2362) --- BaseClasses.py | 5 ----- worlds/alttp/InvertedRegions.py | 2 -- worlds/alttp/ItemPool.py | 2 -- worlds/alttp/Regions.py | 2 -- 4 files changed, 11 deletions(-) diff --git a/BaseClasses.py b/BaseClasses.py index 1b6677dd19..0bd61f68f3 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -328,11 +328,6 @@ class MultiWorld(): """ the base name (without file extension) for each player's output file for a seed """ return f"AP_{self.seed_name}_P{player}_{self.get_file_safe_player_name(player).replace(' ', '_')}" - def initialize_regions(self, regions=None): - for region in regions if regions else self.regions: - region.multiworld = self - self._region_cache[region.player][region.name] = region - @functools.cached_property def world_name_lookup(self): return {self.player_name[player_id]: player_id for player_id in self.player_ids} diff --git a/worlds/alttp/InvertedRegions.py b/worlds/alttp/InvertedRegions.py index ffa23881d3..f89eebec33 100644 --- a/worlds/alttp/InvertedRegions.py +++ b/worlds/alttp/InvertedRegions.py @@ -477,8 +477,6 @@ def create_inverted_regions(world, player): create_lw_region(world, player, 'Death Mountain Bunny Descent Area') ] - world.initialize_regions() - def mark_dark_world_regions(world, player): # cross world caves may have some sections marked as both in_light_world, and in_dark_work. diff --git a/worlds/alttp/ItemPool.py b/worlds/alttp/ItemPool.py index f8fdd55ef6..806a420f41 100644 --- a/worlds/alttp/ItemPool.py +++ b/worlds/alttp/ItemPool.py @@ -535,8 +535,6 @@ def set_up_take_anys(world, player): take_any.shop.add_inventory(0, 'Blue Potion', 0, 0) take_any.shop.add_inventory(1, 'Boss Heart Container', 0, 0, create_location=True) - world.initialize_regions() - def get_pool_core(world, player: int): shuffle = world.shuffle[player] diff --git a/worlds/alttp/Regions.py b/worlds/alttp/Regions.py index 8311bc3269..0cc8a3d6a7 100644 --- a/worlds/alttp/Regions.py +++ b/worlds/alttp/Regions.py @@ -382,8 +382,6 @@ def create_regions(world, player): create_dw_region(world, player, 'Dark Death Mountain Bunny Descent Area') ] - world.initialize_regions() - def create_lw_region(world: MultiWorld, player: int, name: str, locations=None, exits=None): return _create_region(world, player, name, LTTPRegionType.LightWorld, 'Light World', locations, exits) From aa73dbab2daa1d316add35809a655d559c9e9940 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Thu, 26 Oct 2023 00:03:14 +0200 Subject: [PATCH 46/54] Subnautica: avoid cache recreation in create_regions call and clean up function. (#2365) --- worlds/subnautica/__init__.py | 40 ++++++++++++++++++++++++----------- 1 file changed, 28 insertions(+), 12 deletions(-) diff --git a/worlds/subnautica/__init__.py b/worlds/subnautica/__init__.py index 7b25b61c81..de4f4e33dc 100644 --- a/worlds/subnautica/__init__.py +++ b/worlds/subnautica/__init__.py @@ -65,22 +65,38 @@ class SubnauticaWorld(World): creature_pool, self.options.creature_scans.value) def create_regions(self): - self.multiworld.regions += [ - self.create_region("Menu", None, ["Lifepod 5"]), - self.create_region("Planet 4546B", - locations.events + - [location["name"] for location in locations.location_table.values()] + - [creature + creatures.suffix for creature in self.creatures_to_scan]) - ] + # Create Regions + menu_region = Region("Menu", self.player, self.multiworld) + planet_region = Region("Planet 4546B", self.player, self.multiworld) - # Link regions - self.multiworld.get_entrance("Lifepod 5", self.player).connect(self.multiworld.get_region("Planet 4546B", self.player)) + # Link regions together + menu_region.connect(planet_region, "Lifepod 5") + + # Create regular locations + location_names = itertools.chain((location["name"] for location in locations.location_table.values()), + (creature + creatures.suffix for creature in self.creatures_to_scan)) + for location_name in location_names: + loc_id = self.location_name_to_id[location_name] + location = SubnauticaLocation(self.player, location_name, loc_id, planet_region) + planet_region.locations.append(location) + + # Create events + goal_event_name = self.options.goal.get_event_name() for event in locations.events: - self.multiworld.get_location(event, self.player).place_locked_item( + location = SubnauticaLocation(self.player, event, None, planet_region) + planet_region.locations.append(location) + location.place_locked_item( SubnauticaItem(event, ItemClassification.progression, None, player=self.player)) - # make the goal event the victory "item" - self.multiworld.get_location(self.options.goal.get_event_name(), self.player).item.name = "Victory" + if event == goal_event_name: + # make the goal event the victory "item" + location.item.name = "Victory" + + # Register regions to multiworld + self.multiworld.regions += [ + menu_region, + planet_region + ] # refer to Rules.py set_rules = set_rules From 88d69dba97badfd04c18a0905aa7de59ee418018 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Thu, 26 Oct 2023 00:51:32 +0200 Subject: [PATCH 47/54] DLCQuest: logic speed up (#2323) --- worlds/dlcquest/Items.py | 2 ++ worlds/dlcquest/Regions.py | 5 +++- worlds/dlcquest/Rules.py | 54 +++++++++++++------------------------ worlds/dlcquest/__init__.py | 19 +++++++++++-- 4 files changed, 42 insertions(+), 38 deletions(-) diff --git a/worlds/dlcquest/Items.py b/worlds/dlcquest/Items.py index 61d1be54cb..e7008f7b12 100644 --- a/worlds/dlcquest/Items.py +++ b/worlds/dlcquest/Items.py @@ -11,6 +11,8 @@ from . import Options, data class DLCQuestItem(Item): game: str = "DLCQuest" + coins: int = 0 + coin_suffix: str = "" offset = 120_000 diff --git a/worlds/dlcquest/Regions.py b/worlds/dlcquest/Regions.py index 402ac722a0..6dad9fc10c 100644 --- a/worlds/dlcquest/Regions.py +++ b/worlds/dlcquest/Regions.py @@ -23,7 +23,10 @@ def add_coin(region: Region, coin: int, player: int, suffix: str): location_coin = f"{region.name}{suffix}" location = DLCQuestLocation(player, location_coin, None, region) region.locations.append(location) - location.place_locked_item(create_event(player, number_coin)) + event = create_event(player, number_coin) + event.coins = coin + event.coin_suffix = suffix + location.place_locked_item(event) def create_regions(multiworld: MultiWorld, player: int, world_options: Options.DLCQuestOptions): diff --git a/worlds/dlcquest/Rules.py b/worlds/dlcquest/Rules.py index c5fdfe8282..a11e5c504e 100644 --- a/worlds/dlcquest/Rules.py +++ b/worlds/dlcquest/Rules.py @@ -7,41 +7,25 @@ from . import Options from .Items import DLCQuestItem -def create_event(player, event: str): +def create_event(player, event: str) -> DLCQuestItem: return DLCQuestItem(event, ItemClassification.progression, None, player) +def has_enough_coin(player: int, coin: int): + return lambda state: state.prog_items[" coins", player] >= coin + + +def has_enough_coin_freemium(player: int, coin: int): + return lambda state: state.prog_items[" coins freemium", player] >= coin + + def set_rules(world, player, World_Options: Options.DLCQuestOptions): - def has_enough_coin(player: int, coin: int): - def has_coin(state, player: int, coins: int): - coin_possessed = 0 - for i in [4, 7, 9, 10, 46, 50, 60, 76, 89, 100, 171, 203]: - name_coin = f"{i} coins" - if state.has(name_coin, player): - coin_possessed += i - - return coin_possessed >= coins - - return lambda state: has_coin(state, player, coin) - - def has_enough_coin_freemium(player: int, coin: int): - def has_coin(state, player: int, coins: int): - coin_possessed = 0 - for i in [20, 50, 90, 95, 130, 150, 154, 200]: - name_coin = f"{i} coins freemium" - if state.has(name_coin, player): - coin_possessed += i - - return coin_possessed >= coins - - return lambda state: has_coin(state, player, coin) - - set_basic_rules(World_Options, has_enough_coin, player, world) - set_lfod_rules(World_Options, has_enough_coin_freemium, player, world) + set_basic_rules(World_Options, player, world) + set_lfod_rules(World_Options, player, world) set_completion_condition(World_Options, player, world) -def set_basic_rules(World_Options, has_enough_coin, player, world): +def set_basic_rules(World_Options, player, world): if World_Options.campaign == Options.Campaign.option_live_freemium_or_die: return set_basic_entrance_rules(player, world) @@ -49,8 +33,8 @@ def set_basic_rules(World_Options, has_enough_coin, player, world): set_basic_shuffled_items_rules(World_Options, player, world) set_double_jump_glitchless_rules(World_Options, player, world) set_easy_double_jump_glitch_rules(World_Options, player, world) - self_basic_coinsanity_funded_purchase_rules(World_Options, has_enough_coin, player, world) - set_basic_self_funded_purchase_rules(World_Options, has_enough_coin, player, world) + self_basic_coinsanity_funded_purchase_rules(World_Options, player, world) + set_basic_self_funded_purchase_rules(World_Options, player, world) self_basic_win_condition(World_Options, player, world) @@ -131,7 +115,7 @@ def set_easy_double_jump_glitch_rules(World_Options, player, world): lambda state: state.has("Double Jump Pack", player)) -def self_basic_coinsanity_funded_purchase_rules(World_Options, has_enough_coin, player, world): +def self_basic_coinsanity_funded_purchase_rules(World_Options, player, world): if World_Options.coinsanity != Options.CoinSanity.option_coin: return number_of_bundle = math.floor(825 / World_Options.coinbundlequantity) @@ -194,7 +178,7 @@ def self_basic_coinsanity_funded_purchase_rules(World_Options, has_enough_coin, math.ceil(5 / World_Options.coinbundlequantity))) -def set_basic_self_funded_purchase_rules(World_Options, has_enough_coin, player, world): +def set_basic_self_funded_purchase_rules(World_Options, player, world): if World_Options.coinsanity != Options.CoinSanity.option_none: return set_rule(world.get_location("Movement Pack", player), @@ -241,14 +225,14 @@ def self_basic_win_condition(World_Options, player, world): player)) -def set_lfod_rules(World_Options, has_enough_coin_freemium, player, world): +def set_lfod_rules(World_Options, player, world): if World_Options.campaign == Options.Campaign.option_basic: return set_lfod_entrance_rules(player, world) set_boss_door_requirements_rules(player, world) set_lfod_self_obtained_items_rules(World_Options, player, world) set_lfod_shuffled_items_rules(World_Options, player, world) - self_lfod_coinsanity_funded_purchase_rules(World_Options, has_enough_coin_freemium, player, world) + self_lfod_coinsanity_funded_purchase_rules(World_Options, player, world) set_lfod_self_funded_purchase_rules(World_Options, has_enough_coin_freemium, player, world) @@ -327,7 +311,7 @@ def set_lfod_shuffled_items_rules(World_Options, player, world): lambda state: state.can_reach("Cut Content", 'region', player)) -def self_lfod_coinsanity_funded_purchase_rules(World_Options, has_enough_coin_freemium, player, world): +def self_lfod_coinsanity_funded_purchase_rules(World_Options, player, world): if World_Options.coinsanity != Options.CoinSanity.option_coin: return number_of_bundle = math.floor(889 / World_Options.coinbundlequantity) diff --git a/worlds/dlcquest/__init__.py b/worlds/dlcquest/__init__.py index 392eac7796..54d27f7b65 100644 --- a/worlds/dlcquest/__init__.py +++ b/worlds/dlcquest/__init__.py @@ -1,6 +1,6 @@ from typing import Union -from BaseClasses import Tutorial +from BaseClasses import Tutorial, CollectionState from worlds.AutoWorld import WebWorld, World from . import Options from .Items import DLCQuestItem, ItemData, create_items, item_table @@ -71,7 +71,6 @@ class DLCqworld(World): if self.options.coinsanity == Options.CoinSanity.option_coin and self.options.coinbundlequantity >= 5: self.multiworld.push_precollected(self.create_item("Movement Pack")) - def create_item(self, item: Union[str, ItemData]) -> DLCQuestItem: if isinstance(item, str): item = item_table[item] @@ -87,3 +86,19 @@ class DLCqworld(World): "seed": self.random.randrange(99999999) }) return options_dict + + def collect(self, state: CollectionState, item: DLCQuestItem) -> bool: + change = super().collect(state, item) + if change: + suffix = item.coin_suffix + if suffix: + state.prog_items[suffix, self.player] += item.coins + return change + + def remove(self, state: CollectionState, item: DLCQuestItem) -> bool: + change = super().remove(state, item) + if change: + suffix = item.coin_suffix + if suffix: + state.prog_items[suffix, self.player] -= item.coins + return change From b16804102d2f32301c38f1a3aacd7054ab764eed Mon Sep 17 00:00:00 2001 From: Bryce Wilson Date: Thu, 26 Oct 2023 18:55:46 -0700 Subject: [PATCH 48/54] BizHawkClient: Add lock for communicating with lua script (#2369) --- worlds/_bizhawk/__init__.py | 118 ++++++++++++++---------------------- worlds/_bizhawk/context.py | 6 +- 2 files changed, 50 insertions(+), 74 deletions(-) diff --git a/worlds/_bizhawk/__init__.py b/worlds/_bizhawk/__init__.py index cdf227ec7b..3403990832 100644 --- a/worlds/_bizhawk/__init__.py +++ b/worlds/_bizhawk/__init__.py @@ -13,7 +13,6 @@ import typing BIZHAWK_SOCKET_PORT = 43055 -EXPECTED_SCRIPT_VERSION = 1 class ConnectionStatus(enum.IntEnum): @@ -22,15 +21,6 @@ class ConnectionStatus(enum.IntEnum): CONNECTED = 3 -class BizHawkContext: - streams: typing.Optional[typing.Tuple[asyncio.StreamReader, asyncio.StreamWriter]] - connection_status: ConnectionStatus - - def __init__(self) -> None: - self.streams = None - self.connection_status = ConnectionStatus.NOT_CONNECTED - - class NotConnectedError(Exception): """Raised when something tries to make a request to the connector script before a connection has been established""" pass @@ -51,6 +41,50 @@ class SyncError(Exception): pass +class BizHawkContext: + streams: typing.Optional[typing.Tuple[asyncio.StreamReader, asyncio.StreamWriter]] + connection_status: ConnectionStatus + _lock: asyncio.Lock + + def __init__(self) -> None: + self.streams = None + self.connection_status = ConnectionStatus.NOT_CONNECTED + self._lock = asyncio.Lock() + + async def _send_message(self, message: str): + async with self._lock: + if self.streams is None: + raise NotConnectedError("You tried to send a request before a connection to BizHawk was made") + + try: + reader, writer = self.streams + writer.write(message.encode("utf-8") + b"\n") + await asyncio.wait_for(writer.drain(), timeout=5) + + res = await asyncio.wait_for(reader.readline(), timeout=5) + + if res == b"": + writer.close() + self.streams = None + self.connection_status = ConnectionStatus.NOT_CONNECTED + raise RequestFailedError("Connection closed") + + if self.connection_status == ConnectionStatus.TENTATIVE: + self.connection_status = ConnectionStatus.CONNECTED + + return res.decode("utf-8") + except asyncio.TimeoutError as exc: + writer.close() + self.streams = None + self.connection_status = ConnectionStatus.NOT_CONNECTED + raise RequestFailedError("Connection timed out") from exc + except ConnectionResetError as exc: + writer.close() + self.streams = None + self.connection_status = ConnectionStatus.NOT_CONNECTED + raise RequestFailedError("Connection reset") from exc + + async def connect(ctx: BizHawkContext) -> bool: """Attempts to establish a connection with the connector script. Returns True if successful.""" try: @@ -72,74 +106,14 @@ def disconnect(ctx: BizHawkContext) -> None: async def get_script_version(ctx: BizHawkContext) -> int: - if ctx.streams is None: - raise NotConnectedError("You tried to send a request before a connection to BizHawk was made") - - try: - reader, writer = ctx.streams - writer.write("VERSION".encode("ascii") + b"\n") - await asyncio.wait_for(writer.drain(), timeout=5) - - version = await asyncio.wait_for(reader.readline(), timeout=5) - - if version == b"": - writer.close() - ctx.streams = None - ctx.connection_status = ConnectionStatus.NOT_CONNECTED - raise RequestFailedError("Connection closed") - - return int(version.decode("ascii")) - except asyncio.TimeoutError as exc: - writer.close() - ctx.streams = None - ctx.connection_status = ConnectionStatus.NOT_CONNECTED - raise RequestFailedError("Connection timed out") from exc - except ConnectionResetError as exc: - writer.close() - ctx.streams = None - ctx.connection_status = ConnectionStatus.NOT_CONNECTED - raise RequestFailedError("Connection reset") from exc + return int(await ctx._send_message("VERSION")) async def send_requests(ctx: BizHawkContext, req_list: typing.List[typing.Dict[str, typing.Any]]) -> typing.List[typing.Dict[str, typing.Any]]: """Sends a list of requests to the BizHawk connector and returns their responses. It's likely you want to use the wrapper functions instead of this.""" - if ctx.streams is None: - raise NotConnectedError("You tried to send a request before a connection to BizHawk was made") - - try: - reader, writer = ctx.streams - writer.write(json.dumps(req_list).encode("utf-8") + b"\n") - await asyncio.wait_for(writer.drain(), timeout=5) - - res = await asyncio.wait_for(reader.readline(), timeout=5) - - if res == b"": - writer.close() - ctx.streams = None - ctx.connection_status = ConnectionStatus.NOT_CONNECTED - raise RequestFailedError("Connection closed") - - if ctx.connection_status == ConnectionStatus.TENTATIVE: - ctx.connection_status = ConnectionStatus.CONNECTED - - ret = json.loads(res.decode("utf-8")) - for response in ret: - if response["type"] == "ERROR": - raise ConnectorError(response["err"]) - - return ret - except asyncio.TimeoutError as exc: - writer.close() - ctx.streams = None - ctx.connection_status = ConnectionStatus.NOT_CONNECTED - raise RequestFailedError("Connection timed out") from exc - except ConnectionResetError as exc: - writer.close() - ctx.streams = None - ctx.connection_status = ConnectionStatus.NOT_CONNECTED - raise RequestFailedError("Connection reset") from exc + return json.loads(await ctx._send_message(json.dumps(req_list))) async def ping(ctx: BizHawkContext) -> None: diff --git a/worlds/_bizhawk/context.py b/worlds/_bizhawk/context.py index 465334274e..5d865f3321 100644 --- a/worlds/_bizhawk/context.py +++ b/worlds/_bizhawk/context.py @@ -13,8 +13,8 @@ from CommonClient import CommonContext, ClientCommandProcessor, get_base_parser, import Patch import Utils -from . import BizHawkContext, ConnectionStatus, RequestFailedError, connect, disconnect, get_hash, get_script_version, \ - get_system, ping +from . import BizHawkContext, ConnectionStatus, NotConnectedError, RequestFailedError, connect, disconnect, get_hash, \ + get_script_version, get_system, ping from .client import BizHawkClient, AutoBizHawkClientRegister @@ -133,6 +133,8 @@ async def _game_watcher(ctx: BizHawkClientContext): except RequestFailedError as exc: logger.info(f"Lost connection to BizHawk: {exc.args[0]}") continue + except NotConnectedError: + continue # Get slot name and send `Connect` if ctx.server is not None and ctx.username is None: From 6061bffbb670c67f96c9fd164ff949e4f6ba4271 Mon Sep 17 00:00:00 2001 From: Justus Lind Date: Fri, 27 Oct 2023 14:12:04 +1000 Subject: [PATCH 49/54] Pokemon R/B: Avoid a case of repeatedly checking of state in ER (#2376) --- worlds/pokemon_rb/regions.py | 23 +++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/worlds/pokemon_rb/regions.py b/worlds/pokemon_rb/regions.py index 1816d010c0..f844976548 100644 --- a/worlds/pokemon_rb/regions.py +++ b/worlds/pokemon_rb/regions.py @@ -1594,7 +1594,7 @@ def create_regions(self): connect(multiworld, player, "Menu", "Pallet Town", one_way=True) connect(multiworld, player, "Menu", "Pokedex", one_way=True) connect(multiworld, player, "Menu", "Evolution", one_way=True) - connect(multiworld, player, "Menu", "Fossil", lambda state: logic.fossil_checks(state, + connect(multiworld, player, "Menu", "Fossil", lambda state: logic.fossil_checks(state, state.multiworld.second_fossil_check_condition[player].value, player), one_way=True) connect(multiworld, player, "Pallet Town", "Route 1") connect(multiworld, player, "Route 1", "Viridian City") @@ -2269,23 +2269,28 @@ def create_regions(self): event_locations = self.multiworld.get_filled_locations(player) - def adds_reachable_entrances(entrances_copy, item): + def adds_reachable_entrances(entrances_copy, item, dead_end_cache): + ret = dead_end_cache.get(item.name) + if (ret != None): + return ret + state_copy = state.copy() state_copy.collect(item, True) state.sweep_for_events(locations=event_locations) ret = len([entrance for entrance in entrances_copy if entrance in reachable_entrances or entrance.parent_region.can_reach(state_copy)]) > len(reachable_entrances) + dead_end_cache[item.name] = ret return ret - def dead_end(entrances_copy, e): + def dead_end(entrances_copy, e, dead_end_cache): region = e.parent_region check_warps = set() checked_regions = {region} check_warps.update(region.exits) check_warps.remove(e) for location in region.locations: - if location.item and location.item.name in relevant_events and adds_reachable_entrances(entrances_copy, - location.item): + if location.item and location.item.name in relevant_events and \ + adds_reachable_entrances(entrances_copy, location.item, dead_end_cache): return False while check_warps: warp = check_warps.pop() @@ -2302,7 +2307,7 @@ def create_regions(self): check_warps.update(warp.connected_region.exits) for location in warp.connected_region.locations: if (location.item and location.item.name in relevant_events and - adds_reachable_entrances(entrances_copy, location.item)): + adds_reachable_entrances(entrances_copy, location.item, dead_end_cache)): return False return True @@ -2332,6 +2337,8 @@ def create_regions(self): if multiworld.door_shuffle[player] == "full" or len(entrances) != len(reachable_entrances): entrances.sort(key=lambda e: e.name not in entrance_only) + dead_end_cache = {} + # entrances list is empty while it's being sorted, must pass a copy to iterate through entrances_copy = entrances.copy() if multiworld.door_shuffle[player] == "decoupled": @@ -2342,10 +2349,10 @@ def create_regions(self): elif len(reachable_entrances) > (1 if multiworld.door_shuffle[player] == "insanity" else 8) and len( entrances) <= (starting_entrances - 3): entrances.sort(key=lambda e: 0 if e in reachable_entrances else 2 if - dead_end(entrances_copy, e) else 1) + dead_end(entrances_copy, e, dead_end_cache) else 1) else: entrances.sort(key=lambda e: 0 if e in reachable_entrances else 1 if - dead_end(entrances_copy, e) else 2) + dead_end(entrances_copy, e, dead_end_cache) else 2) if multiworld.door_shuffle[player] == "full": outdoor = outdoor_map(entrances[0].parent_region.name) if len(entrances) < 48 and not outdoor: From 0f7ebe389e0d08419b79684c9816702e34c39d19 Mon Sep 17 00:00:00 2001 From: Bryce Wilson Date: Thu, 26 Oct 2023 21:14:25 -0700 Subject: [PATCH 50/54] BizHawkClient: Add better launcher component suffix handling (#2367) --- worlds/LauncherComponents.py | 3 --- worlds/_bizhawk/client.py | 38 +++++++++++++++++++++++++----------- 2 files changed, 27 insertions(+), 14 deletions(-) diff --git a/worlds/LauncherComponents.py b/worlds/LauncherComponents.py index 2d445a77b8..c3ae2b0495 100644 --- a/worlds/LauncherComponents.py +++ b/worlds/LauncherComponents.py @@ -89,9 +89,6 @@ components: List[Component] = [ Component('SNI Client', 'SNIClient', file_identifier=SuffixIdentifier('.apz3', '.apm3', '.apsoe', '.aplttp', '.apsm', '.apsmz3', '.apdkc3', '.apsmw', '.apl2ac')), - # BizHawk - Component("BizHawk Client", "BizHawkClient", component_type=Type.CLIENT, - file_identifier=SuffixIdentifier()), Component('Links Awakening DX Client', 'LinksAwakeningClient', file_identifier=SuffixIdentifier('.apladx')), Component('LttP Adjuster', 'LttPAdjuster'), diff --git a/worlds/_bizhawk/client.py b/worlds/_bizhawk/client.py index b614c083ba..32a6e3704e 100644 --- a/worlds/_bizhawk/client.py +++ b/worlds/_bizhawk/client.py @@ -16,12 +16,22 @@ else: BizHawkClientContext = object +def launch_client(*args) -> None: + from .context import launch + launch_subprocess(launch, name="BizHawkClient") + +component = Component("BizHawk Client", "BizHawkClient", component_type=Type.CLIENT, func=launch_client, + file_identifier=SuffixIdentifier()) +components.append(component) + + class AutoBizHawkClientRegister(abc.ABCMeta): game_handlers: ClassVar[Dict[Tuple[str, ...], Dict[str, BizHawkClient]]] = {} def __new__(cls, name: str, bases: Tuple[type, ...], namespace: Dict[str, Any]) -> AutoBizHawkClientRegister: new_class = super().__new__(cls, name, bases, namespace) + # Register handler if "system" in namespace: systems = (namespace["system"],) if type(namespace["system"]) is str else tuple(sorted(namespace["system"])) if systems not in AutoBizHawkClientRegister.game_handlers: @@ -30,6 +40,19 @@ class AutoBizHawkClientRegister(abc.ABCMeta): if "game" in namespace: AutoBizHawkClientRegister.game_handlers[systems][namespace["game"]] = new_class() + # Update launcher component's suffixes + if "patch_suffix" in namespace: + if namespace["patch_suffix"] is not None: + existing_identifier: SuffixIdentifier = component.file_identifier + new_suffixes = [*existing_identifier.suffixes] + + if type(namespace["patch_suffix"]) is str: + new_suffixes.append(namespace["patch_suffix"]) + else: + new_suffixes.extend(namespace["patch_suffix"]) + + component.file_identifier = SuffixIdentifier(*new_suffixes) + return new_class @staticmethod @@ -45,11 +68,14 @@ class AutoBizHawkClientRegister(abc.ABCMeta): class BizHawkClient(abc.ABC, metaclass=AutoBizHawkClientRegister): system: ClassVar[Union[str, Tuple[str, ...]]] - """The system that the game this client is for runs on""" + """The system(s) that the game this client is for runs on""" game: ClassVar[str] """The game this client is for""" + patch_suffix: ClassVar[Optional[Union[str, Tuple[str, ...]]]] + """The file extension(s) this client is meant to open and patch (e.g. ".apz3")""" + @abc.abstractmethod async def validate_rom(self, ctx: BizHawkClientContext) -> bool: """Should return whether the currently loaded ROM should be handled by this client. You might read the game name @@ -75,13 +101,3 @@ class BizHawkClient(abc.ABC, metaclass=AutoBizHawkClientRegister): def on_package(self, ctx: BizHawkClientContext, cmd: str, args: dict) -> None: """For handling packages from the server. Called from `BizHawkClientContext.on_package`.""" pass - - -def launch_client(*args) -> None: - from .context import launch - launch_subprocess(launch, name="BizHawkClient") - - -if not any(component.script_name == "BizHawkClient" for component in components): - components.append(Component("BizHawk Client", "BizHawkClient", component_type=Type.CLIENT, func=launch_client, - file_identifier=SuffixIdentifier())) From 3b5f9d175885350c4a1ff2fb66c6db784f651644 Mon Sep 17 00:00:00 2001 From: Jarno Date: Fri, 27 Oct 2023 12:01:46 +0200 Subject: [PATCH 51/54] Timespinner: Fixed generation error caused by new options system (#2374) --- worlds/timespinner/__init__.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/worlds/timespinner/__init__.py b/worlds/timespinner/__init__.py index de1d58e961..24230862bd 100644 --- a/worlds/timespinner/__init__.py +++ b/worlds/timespinner/__init__.py @@ -49,11 +49,9 @@ class TimespinnerWorld(World): precalculated_weights: PreCalculatedWeights - def __init__(self, world: MultiWorld, player: int): - super().__init__(world, player) - self.precalculated_weights = PreCalculatedWeights(world, player) - def generate_early(self) -> None: + self.precalculated_weights = PreCalculatedWeights(self.multiworld, self.player) + # in generate_early the start_inventory isnt copied over to precollected_items yet, so we can still modify the options directly if self.multiworld.start_inventory[self.player].value.pop('Meyef', 0) > 0: self.multiworld.StartWithMeyef[self.player].value = self.multiworld.StartWithMeyef[self.player].option_true From 16fe66721f9c2c6871e73f251b1fc76c00be0240 Mon Sep 17 00:00:00 2001 From: Aaron Wagener Date: Fri, 27 Oct 2023 05:12:17 -0500 Subject: [PATCH 52/54] Stardew Valley: Use the pre-existing cache rather than ignoring it (#2368) --- worlds/stardew_valley/__init__.py | 4 ++-- worlds/stardew_valley/regions.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/worlds/stardew_valley/__init__.py b/worlds/stardew_valley/__init__.py index 1f46eb79d7..177b6436ae 100644 --- a/worlds/stardew_valley/__init__.py +++ b/worlds/stardew_valley/__init__.py @@ -100,15 +100,15 @@ class StardewValleyWorld(World): return region world_regions, self.randomized_entrances = create_regions(create_region, self.multiworld.random, self.options) - self.multiworld.regions.extend(world_regions) def add_location(name: str, code: Optional[int], region: str): - region = self.multiworld.get_region(region, self.player) + region = world_regions[region] location = StardewLocation(self.player, name, code, region) location.access_rule = lambda _: True region.locations.append(location) create_locations(add_location, self.options, self.multiworld.random) + self.multiworld.regions.extend(world_regions.values()) def create_items(self): self.precollect_starting_season() diff --git a/worlds/stardew_valley/regions.py b/worlds/stardew_valley/regions.py index e8daa772d8..d8e2248411 100644 --- a/worlds/stardew_valley/regions.py +++ b/worlds/stardew_valley/regions.py @@ -429,7 +429,7 @@ def create_final_connections(world_options) -> List[ConnectionData]: def create_regions(region_factory: RegionFactory, random: Random, world_options) -> Tuple[ - Iterable[Region], Dict[str, str]]: + Dict[str, Region], Dict[str, str]]: final_regions = create_final_regions(world_options) regions: Dict[str: Region] = {region.name: region_factory(region.name, region.exits) for region in final_regions} @@ -444,7 +444,7 @@ def create_regions(region_factory: RegionFactory, random: Random, world_options) if connection.name in entrances: entrances[connection.name].connect(regions[connection.destination]) - return regions.values(), randomized_data + return regions, randomized_data def randomize_connections(random: Random, world_options, regions_by_name) -> Tuple[ From d595b1a67f72aee26d921c43dd07fc06c15d616c Mon Sep 17 00:00:00 2001 From: Aaron Wagener Date: Fri, 27 Oct 2023 05:30:32 -0500 Subject: [PATCH 53/54] Docs: slight adding games.md rework (#1192) * begin reworking adding games.md * make it presentable * some doc cleanup * style cleanup * rework the "more on that later" section of SDV * remove now unused images * make the doc links consistent * typo Co-authored-by: el-u <109771707+el-u@users.noreply.github.com> --------- Co-authored-by: el-u <109771707+el-u@users.noreply.github.com> --- docs/adding games.md | 437 ++++++++---------- .../archipelago-world-directory-example.png | Bin 51175 -> 0 bytes docs/img/example-init-py-file.png | Bin 82760 -> 0 bytes docs/img/example-items-py-file.png | Bin 48373 -> 0 bytes docs/img/example-locations-py-file.png | Bin 66617 -> 0 bytes docs/img/example-options-py-file.png | Bin 40852 -> 0 bytes docs/img/example-regions-py-file.png | Bin 65088 -> 0 bytes docs/img/example-rules-py-file.png | Bin 84761 -> 0 bytes docs/img/heavy-bullets-managed-directory.png | Bin 84024 -> 0 bytes 9 files changed, 181 insertions(+), 256 deletions(-) delete mode 100644 docs/img/archipelago-world-directory-example.png delete mode 100644 docs/img/example-init-py-file.png delete mode 100644 docs/img/example-items-py-file.png delete mode 100644 docs/img/example-locations-py-file.png delete mode 100644 docs/img/example-options-py-file.png delete mode 100644 docs/img/example-regions-py-file.png delete mode 100644 docs/img/example-rules-py-file.png delete mode 100644 docs/img/heavy-bullets-managed-directory.png diff --git a/docs/adding games.md b/docs/adding games.md index 24d9e499cd..e9f7860fc6 100644 --- a/docs/adding games.md +++ b/docs/adding games.md @@ -1,214 +1,206 @@ +# How do I add a game to Archipelago? - -# How do I add a game to Archipelago? This guide is going to try and be a broad summary of how you can do just that. -There are two key steps to incorporating a game into Archipelago: -- Game Modification +There are two key steps to incorporating a game into Archipelago: + +- Game Modification - Archipelago Server Integration Refer to the following documents as well: -- [network protocol.md](https://github.com/ArchipelagoMW/Archipelago/blob/main/docs/network%20protocol.md) for network communication between client and server. -- [world api.md](https://github.com/ArchipelagoMW/Archipelago/blob/main/docs/world%20api.md) for documentation on server side code and creating a world package. +- [network protocol.md](/docs/network%20protocol.md) for network communication between client and server. +- [world api.md](/docs/world%20api.md) for documentation on server side code and creating a world package. -# Game Modification -One half of the work required to integrate a game into Archipelago is the development of the game client. This is +# Game Modification + +One half of the work required to integrate a game into Archipelago is the development of the game client. This is typically done through a modding API or other modification process, described further down. As an example, modifications to a game typically include (more on this later): + - Hooking into when a 'location check' is completed. - Networking with the Archipelago server. - Optionally, UI or HUD updates to show status of the multiworld session or Archipelago server connection. In order to determine how to modify a game, refer to the following sections. - -## Engine Identification -This is a good way to make the modding process much easier. Being able to identify what engine a game was made in is critical. The first step is to look at a game's files. Let's go over what some game files might look like. It’s important that you be able to see file extensions, so be sure to enable that feature in your file viewer of choice. + +## Engine Identification + +This is a good way to make the modding process much easier. Being able to identify what engine a game was made in is +critical. The first step is to look at a game's files. Let's go over what some game files might look like. It’s +important that you be able to see file extensions, so be sure to enable that feature in your file viewer of choice. Examples are provided below. - + ### Creepy Castle -![Creepy Castle Root Directory in Window's Explorer](./img/creepy-castle-directory.png) - + +![Creepy Castle Root Directory in Windows Explorer](/docs/img/creepy-castle-directory.png) + This is the delightful title Creepy Castle, which is a fantastic game that I highly recommend. It’s also your worst-case -scenario as a modder. All that’s present here is an executable file and some meta-information that Steam uses. You have -basically nothing here to work with. If you want to change this game, the only option you have is to do some pretty nasty -disassembly and reverse engineering work, which is outside the scope of this tutorial. Let’s look at some other examples -of game releases. +scenario as a modder. All that’s present here is an executable file and some meta-information that Steam uses. You have +basically nothing here to work with. If you want to change this game, the only option you have is to do some pretty +nasty disassembly and reverse engineering work, which is outside the scope of this tutorial. Let’s look at some other +examples of game releases. ### Heavy Bullets -![Heavy Bullets Root Directory in Window's Explorer](./img/heavy-bullets-directory.png) - -Here’s the release files for another game, Heavy Bullets. We see a .exe file, like expected, and a few more files. -“hello.txt” is a text file, which we can quickly skim in any text editor. Many games have them in some form, usually -with a name like README.txt, and they may contain information about a game, such as a EULA, terms of service, licensing -information, credits, and general info about the game. You usually won’t find anything too helpful here, but it never -hurts to check. In this case, it contains some credits and a changelog for the game, so nothing too important. -“steam_api.dll” is a file you can safely ignore, it’s just some code used to interface with Steam. -The directory “HEAVY_BULLETS_Data”, however, has some good news. - -![Heavy Bullets Data Directory in Window's Explorer](./img/heavy-bullets-data-directory.png) - -Jackpot! It might not be obvious what you’re looking at here, but I can instantly tell from this folder’s contents that -what we have is a game made in the Unity Engine. If you look in the sub-folders, you’ll seem some .dll files which affirm -our suspicions. Telltale signs for this are directories titled “Managed” and “Mono”, as well as the numbered, extension-less -level files and the sharedassets files. We’ll tell you a bit about why seeing a Unity game is such good news later, -but for now, this is what one looks like. Also keep your eyes out for an executable with a name like UnityCrashHandler, -that’s another dead giveaway. + +![Heavy Bullets Root Directory in Window's Explorer](/docs/img/heavy-bullets-directory.png) + +Here’s the release files for another game, Heavy Bullets. We see a .exe file, like expected, and a few more files. +“hello.txt” is a text file, which we can quickly skim in any text editor. Many games have them in some form, usually +with a name like README.txt, and they may contain information about a game, such as a EULA, terms of service, licensing +information, credits, and general info about the game. You usually won’t find anything too helpful here, but it never +hurts to check. In this case, it contains some credits and a changelog for the game, so nothing too important. +“steam_api.dll” is a file you can safely ignore, it’s just some code used to interface with Steam. +The directory “HEAVY_BULLETS_Data”, however, has some good news. + +![Heavy Bullets Data Directory in Window's Explorer](/docs/img/heavy-bullets-data-directory.png) + +Jackpot! It might not be obvious what you’re looking at here, but I can instantly tell from this folder’s contents that +what we have is a game made in the Unity Engine. If you look in the sub-folders, you’ll seem some .dll files which +affirm our suspicions. Telltale signs for this are directories titled “Managed” and “Mono”, as well as the numbered, +extension-less level files and the sharedassets files. If you've identified the game as a Unity game, some useful tools +and information to help you on your journey can be found at this +[Unity Game Hacking guide.](https://github.com/imadr/Unity-game-hacking) ### Stardew Valley -![Stardew Valley Root Directory in Window's Explorer](./img/stardew-valley-directory.png) - -This is the game contents of Stardew Valley. A lot more to look at here, but some key takeaways. -Notice the .dll files which include “CSharp” in their name. This tells us that the game was made in C#, which is good news. -More on that later. + +![Stardew Valley Root Directory in Window's Explorer](/docs/img/stardew-valley-directory.png) + +This is the game contents of Stardew Valley. A lot more to look at here, but some key takeaways. +Notice the .dll files which include “CSharp” in their name. This tells us that the game was made in C#, which is good +news. Many games made in C# can be modified using the same tools found in our Unity game hacking toolset; namely BepInEx +and MonoMod. ### Gato Roboto -![Gato Roboto Root Directory in Window's Explorer](./img/gato-roboto-directory.png) - -Our last example is the game Gato Roboto. This game is made in GameMaker, which is another green flag to look out for. -The giveaway is the file titled "data.win". This immediately tips us off that this game was made in GameMaker. - -This isn't all you'll ever see looking at game files, but it's a good place to start. -As a general rule, the more files a game has out in plain sight, the more you'll be able to change. -This especially applies in the case of code or script files - always keep a lookout for anything you can use to your -advantage! - + +![Gato Roboto Root Directory in Window's Explorer](/docs/img/gato-roboto-directory.png) + +Our last example is the game Gato Roboto. This game is made in GameMaker, which is another green flag to look out for. +The giveaway is the file titled "data.win". This immediately tips us off that this game was made in GameMaker. For +modifying GameMaker games the [Undertale Mod Tool](https://github.com/krzys-h/UndertaleModTool) is incredibly helpful. + +This isn't all you'll ever see looking at game files, but it's a good place to start. +As a general rule, the more files a game has out in plain sight, the more you'll be able to change. +This especially applies in the case of code or script files - always keep a lookout for anything you can use to your +advantage! + ## Open or Leaked Source Games -As a side note, many games have either been made open source, or have had source files leaked at some point. -This can be a boon to any would-be modder, for obvious reasons. -Always be sure to check - a quick internet search for "(Game) Source Code" might not give results often, but when it -does you're going to have a much better time. - + +As a side note, many games have either been made open source, or have had source files leaked at some point. +This can be a boon to any would-be modder, for obvious reasons. Always be sure to check - a quick internet search for +"(Game) Source Code" might not give results often, but when it does, you're going to have a much better time. + Be sure never to distribute source code for games that you decompile or find if you do not have express permission to do so, or to redistribute any materials obtained through similar methods, as this is illegal and unethical. - -## Modifying Release Versions of Games -However, for now we'll assume you haven't been so lucky, and have to work with only what’s sitting in your install directory. -Some developers are kind enough to deliberately leave you ways to alter their games, like modding tools, -but these are often not geared to the kind of work you'll be doing and may not help much. -As a general rule, any modding tool that lets you write actual code is something worth using. - +## Modifying Release Versions of Games + +However, for now we'll assume you haven't been so lucky, and have to work with only what’s sitting in your install +directory. Some developers are kind enough to deliberately leave you ways to alter their games, like modding tools, +but these are often not geared to the kind of work you'll be doing and may not help much. + +As a general rule, any modding tool that lets you write actual code is something worth using. + ### Research -The first step is to research your game. Even if you've been dealt the worst hand in terms of engine modification, -it's possible other motivated parties have concocted useful tools for your game already. -Always be sure to search the Internet for the efforts of other modders. - -### Analysis Tools -Depending on the game’s underlying engine, there may be some tools you can use either in lieu of or in addition to existing game tools. - -#### [dnSpy](https://github.com/dnSpy/dnSpy/releases) -The first tool in your toolbox is dnSpy. -dnSpy is useful for opening and modifying code files, like .exe and .dll files, that were made in C#. -This won't work for executable files made by other means, and obfuscated code (code which was deliberately made -difficult to reverse engineer) will thwart it, but 9 times out of 10 this is exactly what you need. -You'll want to avoid opening common library files in dnSpy, as these are unlikely to contain the data you're looking to -modify. -For Unity games, the file you’ll want to open will be the file (Data Folder)/Managed/Assembly-CSharp.dll, as pictured below: - -![Heavy Bullets Managed Directory in Window's Explorer](./img/heavy-bullets-managed-directory.png) - -This file will contain the data of the actual game. -For other C# games, the file you want is usually just the executable itself. - -With dnSpy, you can view the game’s C# code, but the tool isn’t perfect. -Although the names of classes, methods, variables, and more will be preserved, code structures may not remain entirely intact. This is because compilers will often subtly rewrite code to be more optimal, so that it works the same as the original code but uses fewer resources. Compiled C# files also lose comments and other documentation. - -#### [UndertaleModTool](https://github.com/krzys-h/UndertaleModTool/releases) -This is currently the best tool for modifying games made in GameMaker, and supports games made in both GMS 1 and 2. -It allows you to modify code in GML, if the game wasn't made with the wrong compiler (usually something you don't have -to worry about). +The first step is to research your game. Even if you've been dealt the worst hand in terms of engine modification, +it's possible other motivated parties have concocted useful tools for your game already. +Always be sure to search the Internet for the efforts of other modders. -You'll want to open the data.win file, as this is where all the goods are kept. -Like dnSpy, you won’t be able to see comments. -In addition, you will be able to see and modify many hidden fields on items that GameMaker itself will often hide from -creators. +### Other helpful tools -Fonts in particular are notoriously complex, and to add new sprites you may need to modify existing sprite sheets. - -#### [CheatEngine](https://cheatengine.org/) -CheatEngine is a tool with a very long and storied history. -Be warned that because it performs live modifications to the memory of other processes, it will likely be flagged as -malware (because this behavior is most commonly found in malware and rarely used by other programs). -If you use CheatEngine, you need to have a deep understanding of how computers work at the nuts and bolts level, -including binary data formats, addressing, and assembly language programming. +Depending on the game’s underlying engine, there may be some tools you can use either in lieu of or in addition to +existing game tools. -The tool itself is highly complex and even I have not yet charted its expanses. +#### [CheatEngine](https://cheatengine.org/) + +CheatEngine is a tool with a very long and storied history. +Be warned that because it performs live modifications to the memory of other processes, it will likely be flagged as +malware (because this behavior is most commonly found in malware and rarely used by other programs). +If you use CheatEngine, you need to have a deep understanding of how computers work at the nuts and bolts level, +including binary data formats, addressing, and assembly language programming. + +The tool itself is highly complex and even I have not yet charted its expanses. However, it can also be a very powerful tool in the right hands, allowing you to query and modify gamestate without ever -modifying the actual game itself. -In theory it is compatible with any piece of software you can run on your computer, but there is no "easy way" to do -anything with it. - -### What Modifications You Should Make to the Game -We talked about this briefly in [Game Modification](#game-modification) section. -The next step is to know what you need to make the game do now that you can modify it. Here are your key goals: -- Modify the game so that checks are shuffled -- Know when the player has completed a check, and react accordingly -- Listen for messages from the Archipelago server -- Modify the game to display messages from the Archipelago server -- Add interface for connecting to the Archipelago server with passwords and sessions -- Add commands for manually rewarding, re-syncing, releasing, and other actions - -To elaborate, you need to be able to inform the server whenever you check locations, print out messages that you receive -from the server in-game so players can read them, award items when the server tells you to, sync and re-sync when necessary, -avoid double-awarding items while still maintaining game file integrity, and allow players to manually enter commands in -case the client or server make mistakes. +modifying the actual game itself. +In theory it is compatible with any piece of software you can run on your computer, but there is no "easy way" to do +anything with it. + +### What Modifications You Should Make to the Game + +We talked about this briefly in [Game Modification](#game-modification) section. +The next step is to know what you need to make the game do now that you can modify it. Here are your key goals: + +- Know when the player has checked a location, and react accordingly +- Be able to receive items from the server on the fly +- Keep an index for items received in order to resync from disconnections +- Add interface for connecting to the Archipelago server with passwords and sessions +- Add commands for manually rewarding, re-syncing, releasing, and other actions + +Refer to the [Network Protocol documentation](/docs/network%20protocol.md) for how to communicate with Archipelago's +servers. + +## But my Game is a console game. Can I still add it? + +That depends – what console? + +### My Game is a recent game for the PS4/Xbox-One/Nintendo Switch/etc -Refer to the [Network Protocol documentation](./network%20protocol.md) for how to communicate with Archipelago's servers. - -## But my Game is a console game. Can I still add it? -That depends – what console? - -### My Game is a recent game for the PS4/Xbox-One/Nintendo Switch/etc Most games for recent generations of console platforms are inaccessible to the typical modder. It is generally advised that you do not attempt to work with these games as they are difficult to modify and are protected by their copyright -holders. Most modern AAA game studios will provide a modding interface or otherwise deny modifications for their console games. - -### My Game isn’t that old, it’s for the Wii/PS2/360/etc -This is very complex, but doable. -If you don't have good knowledge of stuff like Assembly programming, this is not where you want to learn it. +holders. Most modern AAA game studios will provide a modding interface or otherwise deny modifications for their console +games. + +### My Game isn’t that old, it’s for the Wii/PS2/360/etc + +This is very complex, but doable. +If you don't have good knowledge of stuff like Assembly programming, this is not where you want to learn it. There exist many disassembly and debugging tools, but more recent content may have lackluster support. - -### My Game is a classic for the SNES/Sega Genesis/etc -That’s a lot more feasible. -There are many good tools available for understanding and modifying games on these older consoles, and the emulation -community will have figured out the bulk of the console’s secrets. -Look for debugging tools, but be ready to learn assembly. -Old consoles usually have their own unique dialects of ASM you’ll need to get used to. + +### My Game is a classic for the SNES/Sega Genesis/etc + +That’s a lot more feasible. +There are many good tools available for understanding and modifying games on these older consoles, and the emulation +community will have figured out the bulk of the console’s secrets. +Look for debugging tools, but be ready to learn assembly. +Old consoles usually have their own unique dialects of ASM you’ll need to get used to. Also make sure there’s a good way to interface with a running emulator, since that’s the only way you can connect these older consoles to the Internet. -There are also hardware mods and flash carts, which can do the same things an emulator would when connected to a computer, -but these will require the same sort of interface software to be written in order to work properly - from your perspective -the two won't really look any different. - -### My Game is an exclusive for the Super Baby Magic Dream Boy. It’s this console from the Soviet Union that- -Unless you have a circuit schematic for the Super Baby Magic Dream Boy sitting on your desk, no. +There are also hardware mods and flash carts, which can do the same things an emulator would when connected to a +computer, but these will require the same sort of interface software to be written in order to work properly; from your +perspective the two won't really look any different. + +### My Game is an exclusive for the Super Baby Magic Dream Boy. It’s this console from the Soviet Union that- + +Unless you have a circuit schematic for the Super Baby Magic Dream Boy sitting on your desk, no. Obscurity is your enemy – there will likely be little to no emulator or modding information, and you’d essentially be -working from scratch. - +working from scratch. + ## How to Distribute Game Modifications + **NEVER EVER distribute anyone else's copyrighted work UNLESS THEY EXPLICITLY GIVE YOU PERMISSION TO DO SO!!!** This is a good way to get any project you're working on sued out from under you. The right way to distribute modified versions of a game's binaries, assuming that the licensing terms do not allow you -to copy them wholesale, is as patches. +to copy them wholesale, is as patches. There are many patch formats, which I'll cover in brief. The common theme is that you can’t distribute anything that wasn't made by you. Patches are files that describe how your modified file differs from the original one, thus avoiding the issue of distributing someone else’s original work. -Users who have a copy of the game just need to apply the patch, and those who don’t are unable to play. +Users who have a copy of the game just need to apply the patch, and those who don’t are unable to play. ### Patches #### IPS + IPS patches are a simple list of chunks to replace in the original to generate the output. It is not possible to encode moving of a chunk, so they may inadvertently contain copyrighted material and should be avoided unless you know it's fine. #### UPS, BPS, VCDIFF (xdelta), bsdiff + Other patch formats generate the difference between two streams (delta patches) with varying complexity. This way it is possible to insert bytes or move chunks without including any original data. Bsdiff is highly optimized and includes compression, so this format is used by APBP. @@ -217,6 +209,7 @@ Only a bsdiff module is integrated into AP. If the final patch requires or is ba bsdiff or APBP before adding it to the AP source code as "basepatch.bsdiff4" or "basepatch.apbp". #### APBP Archipelago Binary Patch + Starting with version 4 of the APBP format, this is a ZIP file containing metadata in `archipelago.json` and additional files required by the game / patching process. For ROM-based games the ZIP will include a `delta.bsdiff4` which is the bsdiff between the original and the randomized ROM. @@ -224,121 +217,53 @@ bsdiff between the original and the randomized ROM. To make using APBP easy, they can be generated by inheriting from `worlds.Files.APDeltaPatch`. ### Mod files + Games which support modding will usually just let you drag and drop the mod’s files into a folder somewhere. Mod files come in many forms, but the rules about not distributing other people's content remain the same. They can either be generic and modify the game using a seed or `slot_data` from the AP websocket, or they can be -generated per seed. +generated per seed. If at all possible, it's generally best practice to collect your world information from `slot_data` +so that the users don't have to move files around in order to play. If the mod is generated by AP and is installed from a ZIP file, it may be possible to include APBP metadata for easy integration into the Webhost by inheriting from `worlds.Files.APContainer`. - ## Archipelago Integration -Integrating a randomizer into Archipelago involves a few steps. -There are several things that may need to be done, but the most important is to create an implementation of the -`World` class specific to your game. This implementation should exist as a Python module within the `worlds` folder -in the Archipelago file structure. -This encompasses most of the data for your game – the items available, what checks you have, the logic for reaching those -checks, what options to offer for the player’s yaml file, and the code to initialize all this data. +In order for your game to communicate with the Archipelago server and generate the necessary randomized information, +you must create a world package in the main Archipelago repo. This section will cover the requisites and expectations +and show the basics of a world. More in depth documentation on the available API can be read in +the [world api doc.](/docs/world%20api.md) +For setting up your working environment with Archipelago refer +to [running from source](/docs/running%20from%20source.md) and the [style guide](/docs/style.md). -Here’s an example of what your world module can look like: - -![Example world module directory open in Window's Explorer](./img/archipelago-world-directory-example.png) +### Requirements -The minimum requirements for a new archipelago world are the package itself (the world folder containing a file named `__init__.py`), -which must define a `World` class object for the game with a game name, create an equal number of items and locations with rules, -a win condition, and at least one `Region` object. - -Let's give a quick breakdown of what the contents for these files look like. -This is just one example of an Archipelago world - the way things are done below is not an immutable property of Archipelago. - -### Items.py -This file is used to define the items which exist in a given game. - -![Example Items.py file open in Notepad++](./img/example-items-py-file.png) - -Some important things to note here. The center of our Items.py file is the item_table, which individually lists every -item in the game and associates them with an ItemData. +A world implementation requires a few key things from its implementation -This file is rather skeletal - most of the actual data has been stripped out for simplicity. -Each ItemData gives a numeric ID to associate with the item and a boolean telling us whether the item might allow the -player to do more than they would have been able to before. - -Next there's the item_frequencies. This simply tells Archipelago how many times each item appears in the pool. -Items that appear exactly once need not be listed - Archipelago will interpret absence from this dictionary as meaning -that the item appears once. - -Lastly, note the `lookup_id_to_name` dictionary, which is typically imported and used in your Archipelago `World` -implementation. This is how Archipelago is told about the items in your world. - -### Locations.py -This file lists all locations in the game. - -![Example Locations.py file open in Notepad++](./img/example-locations-py-file.png) - -First is the achievement_table. It lists each location, the region that it can be found in (more on regions later), -and a numeric ID to associate with each location. - -The exclusion table is a series of dictionaries which are used to exclude certain checks from the pool of progression -locations based on user settings, and the events table associates certain specific checks with specific items. - -`lookup_id_to_name` is also present for locations, though this is a separate dictionary, to be clear. - -### Options.py -This file details options to be searched for in a player's YAML settings file. - -![Example Options.py file open in Notepad++](./img/example-options-py-file.png) - -There are several types of option Archipelago has support for. -In our case, we have three separate choices a player can toggle, either On or Off. -You can also have players choose between a number of predefined values, or have them provide a numeric value within a -specified range. - -### Regions.py -This file contains data which defines the world's topology. -In other words, it details how different regions of the game connect to each other. - -![Example Regions.py file open in Notepad++](./img/example-regions-py-file.png) - -`terraria_regions` contains a list of tuples. -The first element of the tuple is the name of the region, and the second is a list of connections that lead out of the region. - -`mandatory_connections` describe where the connection leads. - -Above this data is a function called `link_terraria_structures` which uses our defined regions and connections to create -something more usable for Archipelago, but this has been left out for clarity. - -### Rules.py -This is the file that details rules for what players can and cannot logically be required to do, based on items and settings. - -![Example Rules.py file open in Notepad++](./img/example-rules-py-file.png) - -This is the most complicated part of the job, and is one part of Archipelago that is likely to see some changes in the future. -The first class, called `TerrariaLogic`, is an extension of the `LogicMixin` class. -This is where you would want to define methods for evaluating certain conditions, which would then return a boolean to -indicate whether conditions have been met. Your rule definitions should start with some sort of identifier to delineate it -from other games, as all rules are mixed together due to `LogicMixin`. In our case, `_terraria_rule` would be a better name. - -The method below, `set_rules()`, is where you would assign these functions as "rules", using lambdas to associate these -functions or combinations of them (or any other code that evaluates to a boolean, in my case just the placeholder `True`) -to certain tasks, like checking locations or using entrances. - -### \_\_init\_\_.py -This is the file that actually extends the `World` class, and is where you expose functionality and data to Archipelago. - -![Example \_\_init\_\_.py file open in Notepad++](./img/example-init-py-file.png) - -This is the most important file for the implementation, and technically the only one you need, but it's best to keep this -file as short as possible and use other script files to do most of the heavy lifting. -If you've done things well, this will just be where you assign everything you set up in the other files to their associated -fields in the class being extended. - -This is also a good place to put game-specific quirky behavior that needs to be managed, as it tends to make things a bit -cluttered if you put these things elsewhere. - -The various methods and attributes are documented in `/worlds/AutoWorld.py[World]` and -[world api.md](https://github.com/ArchipelagoMW/Archipelago/blob/main/docs/world%20api.md), -though it is also recommended to look at existing implementations to see how all this works first-hand. -Once you get all that, all that remains to do is test the game and publish your work. -Make sure to check out [world maintainer.md](./world%20maintainer.md) before publishing. +- A folder within `worlds` that contains an `__init__.py` + - This is what defines it as a Python package and how it's able to be imported + into Archipelago's generation system. During generation time only code that is + defined within this file will be run. It's suggested to split up your information + into more files to improve readability, but all of that information can be + imported at its base level within your world. +- A `World` subclass where you create your world and define all of its rules + and the following requirements: + - Your items and locations need a `item_name_to_id` and `location_name_to_id`, + respectively, mapping. + - An `option_definitions` mapping of your game options with the format + `{name: Class}`, where `name` uses Python snake_case. + - You must define your world's `create_item` method, because this may be called + by the generator in certain circumstances + - When creating your world you submit items and regions to the Multiworld. + - These are lists of said objects which you can access at + `self.multiworld.itempool` and `self.multiworld.regions`. Best practice for + adding to these lists is with either `append` or `extend`, where `append` is a + single object and `extend` is a list. + - Do not use `=` as this will delete other worlds' items and regions. + - Regions are containers for holding your world's Locations. + - Locations are where players will "check" for items and must exist within + a region. It's also important for your world's submitted items to be the same as + its submitted locations count. + - You must always have a "Menu" Region from which the generation algorithm + uses to enter the game and access locations. +- Make sure to check out [world maintainer.md](/docs/world%20maintainer.md) before publishing. \ No newline at end of file diff --git a/docs/img/archipelago-world-directory-example.png b/docs/img/archipelago-world-directory-example.png deleted file mode 100644 index ba720f3319b9a21dbb89e59b9395d698c21e67fa..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 51175 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

    +9Ru)s;B2p&w|5 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 J3H0W}e*;7}euDr2 diff --git a/docs/img/example-init-py-file.png b/docs/img/example-init-py-file.png deleted file mode 100644 index 6dd5c3c9380bdfc1d98cf1bb91484d0b62e00f96..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 82760 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�G4^eBwZ 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

    ZDG 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 diff --git a/docs/img/example-locations-py-file.png b/docs/img/example-locations-py-file.png deleted file mode 100644 index 53c4bc1e29055166793b095eb85c4f21620d8cb4..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 66617 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 t^R*> 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


    jUPINF&z_tUaQb#kbxe7>XjqbnJyF%2~b< zj#(6>OwqqLOsg*O>JpS3E_ltRn;Yc8d`>7~L`C@d^Ql;AZwEJAeK0EI+og~fKoEmI z>QhOgTwixl1zxoZYO7(P2TBj_HkvKRrhV_gKBuEP}-zzG!Vb18#Q^}4`hZWk&~0&537@lv+O~~64zykFhBSFxYGS=V?&1;uVTaxRz3cuZi{1{#cDSF0k6FVO!(xBJs=oq4$b5+V z)`K4IC;;)j?<)5mz8w+#b2{liT_`uYQJ@a?sW{@+4S)LeOuA25Q!X7^O z(TcjOtDGLDWDr-RU%o$bwY9!B;+h6SP0c^#<=_&SanI_pTBg=F?}1Y2j5sudt^lOk z`?nii*`8gZ?q-mZ&Az*nm7?hFk*E%pW_>TUuf|-r_E0pl?_46e-Ean^^^MxIAJb~tdaOUEU$#KtQfz4r__C>@A-1?o2fn4{928}H@=T?&(=`vmDSh} z)D4x2?4NaEkF+x^Ct12GF6`P>k7JkYH&BbKg=e+5Jg094J*-AG2=6pHY;{#}DZhIM zbk4s-1-HrQ>xreXecVSe^%^OEhQ}Iyr?Ck@;#YNG$gIlszMoODw>^HZ1}0YTbV$B+ zwW)C;Z~Gi{Nltk6PiurdpMfGOa}$Fa8esYnh`WtDr}=px!&dD|XCYy327!)G4AB6m zbf)t58tmt;`M#kOdk3Je4=9q@Zg%OK?a3^+eow~-Pf{L3N}UpN!tGo%jj(nVLAF{j zjdi$9jYHQp7U%n-(?D0MQofD55g2w@GU%Liw-0N(pgX_Tza@Cx^-`{hyIDucbYXOT zO#L$2%bSoqVC9SsDB@k|wn9wj?kId`mfXGs zh3KIT<>js|o`=JkkF*k>FYT&0f>yhd$4$rtV1sYn?;Y7x(?!nYN-P!YnVR*kN!?4A z(D6y`ve}-=OFox>E_7FNApnWXPN}WSV^5@Qu@4Ews%ybM#O(f{3sgg?pd%rlJ5|D> z(T
    HWDr60>+a$z3C6?Ri7LOp8p&WT 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 E0BOldqyPW_ diff --git a/docs/img/example-regions-py-file.png b/docs/img/example-regions-py-file.png deleted file mode 100644 index a9d05c53fcbf230d8ac946b9b6ef3065d5c64d23..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 65088 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 Date: Fri, 27 Oct 2023 05:33:59 -0500 Subject: [PATCH 54/54] Tests: Add a unit test for slot_data (#2333) * Tests: Add a unit test for slot_data * use NetUtils.encode * modern PEP8 --- test/general/test_implemented.py | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/test/general/test_implemented.py b/test/general/test_implemented.py index 67d0e5ff72..b60bcee467 100644 --- a/test/general/test_implemented.py +++ b/test/general/test_implemented.py @@ -1,6 +1,8 @@ import unittest -from worlds.AutoWorld import AutoWorldRegister +from Fill import distribute_items_restrictive +from NetUtils import encode +from worlds.AutoWorld import AutoWorldRegister, call_all from . import setup_solo_multiworld @@ -31,3 +33,17 @@ class TestImplemented(unittest.TestCase): for method in ("assert_generate",): self.assertFalse(hasattr(world_type, method), f"{method} must be implemented as a @classmethod named stage_{method}.") + + def test_slot_data(self): + """Tests that if a world creates slot data, it's json serializable.""" + for game_name, world_type in AutoWorldRegister.world_types.items(): + # has an await for generate_output which isn't being called + if game_name in {"Ocarina of Time", "Zillion"}: + continue + with self.subTest(game_name): + multiworld = setup_solo_multiworld(world_type) + distribute_items_restrictive(multiworld) + call_all(multiworld, "post_fill") + for key, data in multiworld.worlds[1].fill_slot_data().items(): + self.assertIsInstance(key, str, "keys in slot data must be a string") + self.assertIsInstance(encode(data), str, f"object {type(data).__name__} not serializable.")

    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 z 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