From 1ae0a9b76f994bd4fc8a7e5e7e50856b7bd58d5a Mon Sep 17 00:00:00 2001 From: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> Date: Wed, 22 May 2024 19:58:06 -0400 Subject: [PATCH 01/64] WebHost: Fixing default values for LocationSets (#3374) * Update macros.html * Update macros.html --- WebHostLib/templates/playerOptions/macros.html | 2 +- WebHostLib/templates/weightedOptions/macros.html | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/WebHostLib/templates/playerOptions/macros.html b/WebHostLib/templates/playerOptions/macros.html index c4d97255d85e..b34ac79a029e 100644 --- a/WebHostLib/templates/playerOptions/macros.html +++ b/WebHostLib/templates/playerOptions/macros.html @@ -141,7 +141,7 @@ {% for group_name in world.location_name_groups.keys()|sort %} {% if group_name != "Everywhere" %}
- +
{% endif %} diff --git a/WebHostLib/templates/weightedOptions/macros.html b/WebHostLib/templates/weightedOptions/macros.html index 91474d76960e..5b8944a43887 100644 --- a/WebHostLib/templates/weightedOptions/macros.html +++ b/WebHostLib/templates/weightedOptions/macros.html @@ -142,7 +142,7 @@ {% for group_name in world.location_name_groups.keys()|sort %} {% if group_name != "Everywhere" %}
- +
{% endif %} From b4fec93c820eeb7d093821fc5f58740f7afdba41 Mon Sep 17 00:00:00 2001 From: Scipio Wright Date: Wed, 22 May 2024 20:00:06 -0400 Subject: [PATCH 02/64] Update guide with some linux instructions (#3330) --- worlds/tunic/docs/setup_en.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/worlds/tunic/docs/setup_en.md b/worlds/tunic/docs/setup_en.md index 94a8a0384191..58cc1bcf25f2 100644 --- a/worlds/tunic/docs/setup_en.md +++ b/worlds/tunic/docs/setup_en.md @@ -31,6 +31,8 @@ Download [BepInEx](https://github.com/BepInEx/BepInEx/releases/download/v6.0.0-p If playing on Steam Deck, follow this [guide to set up BepInEx via Proton](https://docs.bepinex.dev/articles/advanced/proton_wine.html). +If playing on Linux, you may be able to add `WINEDLLOVERRIDES="winhttp=n,b" %command%` to your Steam launch options. If this does not work, follow the guide for Steam Deck above. + Extract the contents of the BepInEx .zip file into your TUNIC game directory:
- **Steam**: Steam\steamapps\common\TUNIC
- **PC Game Pass**: XboxGames\Tunic\Content
From 93f63a3e31fb51a97e931ce36d528405478e5b95 Mon Sep 17 00:00:00 2001 From: Bryce Wilson Date: Wed, 22 May 2024 17:01:27 -0700 Subject: [PATCH 03/64] Pokemon Emerald: Fix broken Markdown in spanish setup guide (#3320) * Pokemon Emerald: Fix broken Markdown in spanish setup guide * Pokemon Emerald: Minor formatting in spanish setup guide * oops --- worlds/pokemon_emerald/docs/setup_es.md | 36 ++++++++++++------------- 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/worlds/pokemon_emerald/docs/setup_es.md b/worlds/pokemon_emerald/docs/setup_es.md index 28c3a4a01a65..1d3721862a4f 100644 --- a/worlds/pokemon_emerald/docs/setup_es.md +++ b/worlds/pokemon_emerald/docs/setup_es.md @@ -14,51 +14,51 @@ Una vez que hayas instalado BizHawk, abre `EmuHawk.exe` y cambia las siguientes `NLua+KopiLua` a `Lua+LuaInterface`, luego reinicia EmuHawk. (Si estás usando BizHawk 2.9, puedes saltar este paso.) - En `Config > Customize`, activa la opción "Run in background" para prevenir desconexiones del cliente mientras la aplicación activa no sea EmuHawk. -- Abre el archivo `.gba` en EmuHawk y luego ve a `Config > Controllers…` para configurar los controles. Si no puedes +- Abre el archivo `.gba` en EmuHawk y luego ve a `Config > Controllers…` para configurar los controles. Si no puedes hacer clic en `Controllers…`, debes abrir cualquier ROM `.gba` primeramente. -- Considera limpiar tus macros y atajos en `Config > Hotkeys…` si no quieres usarlas de manera intencional. Para +- Considera limpiar tus macros y atajos en `Config > Hotkeys…` si no quieres usarlas de manera intencional. Para limpiarlas, selecciona el atajo y presiona la tecla Esc. ## Software Opcional -- [Pokémon Emerald AP Tracker](https://github.com/seto10987/Archipelago-Emerald-AP-Tracker/releases/latest), para usar con -[PopTracker](https://github.com/black-sliver/PopTracker/releases) +- [Pokémon Emerald AP Tracker](https://github.com/seto10987/Archipelago-Emerald-AP-Tracker/releases/latest), para usar +con [PopTracker](https://github.com/black-sliver/PopTracker/releases) ## Generando y Parcheando el Juego -1. Crea tu archivo de configuración (YAML). Puedes hacerlo en +1. Crea tu archivo de configuración (YAML). Puedes hacerlo en [Página de Opciones de Pokémon Emerald](../../../games/Pokemon%20Emerald/player-options). -2. Sigue las instrucciones generales de Archipelago para [Generar un juego] -(../../Archipelago/setup/en#generating-a-game). Esto generará un archivo de salida (output file) para ti. Tu archivo -de parche tendrá la extensión de archivo`.apemerald`. +2. Sigue las instrucciones generales de Archipelago para +[Generar un juego](../../Archipelago/setup/en#generating-a-game). Esto generará un archivo de salida (output file) para +ti. Tu archivo de parche tendrá la extensión de archivo `.apemerald`. 3. Abre `ArchipelagoLauncher.exe` 4. Selecciona "Open Patch" en el lado derecho y elige tu archivo de parcheo. 5. Si esta es la primera vez que vas a parchear, se te pedirá que selecciones la ROM sin parchear. 6. Un archivo parcheado con extensión `.gba` será creado en el mismo lugar que el archivo de parcheo. -7. La primera vez que abras un archivo parcheado con el BizHawk Client, se te preguntará donde está localizado +7. La primera vez que abras un archivo parcheado con el BizHawk Client, se te preguntará donde está localizado `EmuHawk.exe` en tu instalación de BizHawk. -Si estás jugando una seed Single-Player y no te interesa el auto-tracking o las pistas, puedes parar aquí, cierra el -cliente, y carga la ROM ya parcheada en cualquier emulador. Pero para partidas multi-worlds y para otras -implementaciones de Archipelago, continúa usando BizHawk como tu emulador +Si estás jugando una seed Single-Player y no te interesa el auto-tracking o las pistas, puedes parar aquí, cierra el +cliente, y carga la ROM ya parcheada en cualquier emulador. Pero para partidas multi-worlds y para otras +implementaciones de Archipelago, continúa usando BizHawk como tu emulador. ## Conectando con el Servidor -Por defecto, al abrir un archivo parcheado, se harán de manera automática 1-5 pasos. Aun así, ten en cuenta lo +Por defecto, al abrir un archivo parcheado, se harán de manera automática 1-5 pasos. Aun así, ten en cuenta lo siguiente en caso de que debas cerrar y volver a abrir la ventana en mitad de la partida por algún motivo. -1. Pokémon Emerald usa el Archipelago BizHawk Client. Si el cliente no se encuentra abierto al abrir la rom +1. Pokémon Emerald usa el Archipelago BizHawk Client. Si el cliente no se encuentra abierto al abrir la rom parcheada, puedes volver a abrirlo desde el Archipelago Launcher. 2. Asegúrate que EmuHawk está corriendo la ROM parcheada. 3. En EmuHawk, ve a `Tools > Lua Console`. Debes tener esta ventana abierta mientras juegas. 4. En la ventana de Lua Console, ve a `Script > Open Script…`. 5. Ve a la carpeta donde está instalado Archipelago y abre `data/lua/connector_bizhawk_generic.lua`. -6. El emulador y el cliente eventualmente se conectarán uno con el otro. La ventana de BizHawk Client indicará que te +6. El emulador y el cliente eventualmente se conectarán uno con el otro. La ventana de BizHawk Client indicará que te has conectado y reconocerá Pokémon Emerald. -7. Para conectar el cliente con el servidor, ingresa la dirección y el puerto de la sala (ej. `archipelago.gg:38281`) +7. Para conectar el cliente con el servidor, ingresa la dirección y el puerto de la sala (ej. `archipelago.gg:38281`) en el campo de texto que se encuentra en la parte superior del cliente y haz click en Connect. -Ahora deberías poder enviar y recibir ítems. Debes seguir estos pasos cada vez que quieras reconectarte. Es seguro +Ahora deberías poder enviar y recibir ítems. Debes seguir estos pasos cada vez que quieras reconectarte. Es seguro jugar de manera offline; se sincronizará todo cuando te vuelvas a conectar. ## Tracking Automático @@ -70,5 +70,5 @@ Pokémon Emerald tiene un Map Tracker completamente funcional que soporta auto-t 2. Coloca la carpeta del Tracker en la carpeta packs/ dentro de la carpeta de instalación del PopTracker. 3. Abre PopTracker, y carga el Pack de Pokémon Emerald Map Tracker. 4. Para utilizar el auto-tracking, haz click en el símbolo "AP" que se encuentra en la parte superior. -5. Entra la dirección del Servidor de Archipelago (la misma a la que te conectaste para jugar), nombre del jugador, y +5. Entra la dirección del Servidor de Archipelago (la misma a la que te conectaste para jugar), nombre del jugador, y contraseña (deja vacío este campo en caso de no utilizar contraseña). From cd160842ba13c935e2824b80d746f9cce92d05a9 Mon Sep 17 00:00:00 2001 From: Bryce Wilson Date: Wed, 22 May 2024 17:03:42 -0700 Subject: [PATCH 04/64] BizHawkClient: Linting/style (#3335) --- worlds/_bizhawk/__init__.py | 2 +- worlds/_bizhawk/client.py | 14 ++++++-------- worlds/_bizhawk/context.py | 8 ++++---- 3 files changed, 11 insertions(+), 13 deletions(-) diff --git a/worlds/_bizhawk/__init__.py b/worlds/_bizhawk/__init__.py index 94a9ce1ddf04..74f2954b984b 100644 --- a/worlds/_bizhawk/__init__.py +++ b/worlds/_bizhawk/__init__.py @@ -103,7 +103,7 @@ async def connect(ctx: BizHawkContext) -> bool: return True except (TimeoutError, ConnectionRefusedError): continue - + # No ports worked ctx.streams = None ctx.connection_status = ConnectionStatus.NOT_CONNECTED diff --git a/worlds/_bizhawk/client.py b/worlds/_bizhawk/client.py index 32a6e3704e1e..00370c277a17 100644 --- a/worlds/_bizhawk/client.py +++ b/worlds/_bizhawk/client.py @@ -2,7 +2,6 @@ A module containing the BizHawkClient base class and metaclass """ - from __future__ import annotations import abc @@ -12,14 +11,13 @@ if TYPE_CHECKING: from .context import BizHawkClientContext -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) @@ -56,7 +54,7 @@ def __new__(cls, name: str, bases: Tuple[type, ...], namespace: Dict[str, Any]) return new_class @staticmethod - async def get_handler(ctx: BizHawkClientContext, system: str) -> Optional[BizHawkClient]: + async def get_handler(ctx: "BizHawkClientContext", system: str) -> Optional[BizHawkClient]: for systems, handlers in AutoBizHawkClientRegister.game_handlers.items(): if system in systems: for handler in handlers.values(): @@ -77,7 +75,7 @@ class BizHawkClient(abc.ABC, metaclass=AutoBizHawkClientRegister): """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: + 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 from the ROM header, for example. This function will only be asked to validate ROMs from the system set by the client class, so you do not need to check the system yourself. @@ -86,18 +84,18 @@ async def validate_rom(self, ctx: BizHawkClientContext) -> bool: as necessary (such as setting `ctx.game = self.game`, modifying `ctx.items_handling`, etc...).""" ... - async def set_auth(self, ctx: BizHawkClientContext) -> None: + async def set_auth(self, ctx: "BizHawkClientContext") -> None: """Should set ctx.auth in anticipation of sending a `Connected` packet. You may override this if you store slot name in your patched ROM. If ctx.auth is not set after calling, the player will be prompted to enter their username.""" pass @abc.abstractmethod - async def game_watcher(self, ctx: BizHawkClientContext) -> None: + async def game_watcher(self, ctx: "BizHawkClientContext") -> None: """Runs on a loop with the approximate interval `ctx.watcher_timeout`. The currently loaded ROM is guaranteed to have passed your validator when this function is called, and the emulator is very likely to be connected.""" ... - def on_package(self, ctx: BizHawkClientContext, cmd: str, args: dict) -> None: + def on_package(self, ctx: "BizHawkClientContext", cmd: str, args: dict) -> None: """For handling packages from the server. Called from `BizHawkClientContext.on_package`.""" pass diff --git a/worlds/_bizhawk/context.py b/worlds/_bizhawk/context.py index 05bee23412d5..0a28a47894d4 100644 --- a/worlds/_bizhawk/context.py +++ b/worlds/_bizhawk/context.py @@ -3,7 +3,6 @@ checking or launching the client, otherwise it will probably cause circular import issues. """ - import asyncio import enum import subprocess @@ -77,7 +76,7 @@ def on_package(self, cmd, args): if self.client_handler is not None: self.client_handler.on_package(self, cmd, args) - async def server_auth(self, password_requested: bool = False): + async def server_auth(self, password_requested: bool=False): self.password_requested = password_requested if self.bizhawk_ctx.connection_status != ConnectionStatus.CONNECTED: @@ -103,7 +102,7 @@ async def server_auth(self, password_requested: bool = False): await self.send_connect() self.auth_status = AuthStatus.PENDING - async def disconnect(self, allow_autoreconnect: bool = False): + async def disconnect(self, allow_autoreconnect: bool=False): self.auth_status = AuthStatus.NOT_AUTHENTICATED await super().disconnect(allow_autoreconnect) @@ -148,7 +147,8 @@ async def _game_watcher(ctx: BizHawkClientContext): script_version = await get_script_version(ctx.bizhawk_ctx) if script_version != EXPECTED_SCRIPT_VERSION: - logger.info(f"Connector script is incompatible. Expected version {EXPECTED_SCRIPT_VERSION} but got {script_version}. Disconnecting.") + logger.info(f"Connector script is incompatible. Expected version {EXPECTED_SCRIPT_VERSION} but " + f"got {script_version}. Disconnecting.") disconnect(ctx.bizhawk_ctx) continue From 02d3fdf2a6f3ccdf7abdbd540fe57a1acc2568e9 Mon Sep 17 00:00:00 2001 From: Scipio Wright Date: Wed, 22 May 2024 20:05:21 -0400 Subject: [PATCH 05/64] Update options to look better on webhost after update, also give death link a description (#3329) --- worlds/noita/options.py | 57 +++++++++++++++++++++++++++++------------ 1 file changed, 41 insertions(+), 16 deletions(-) diff --git a/worlds/noita/options.py b/worlds/noita/options.py index f2ccbfbc4d3b..0fdd62365a5a 100644 --- a/worlds/noita/options.py +++ b/worlds/noita/options.py @@ -3,11 +3,13 @@ class PathOption(Choice): - """Choose where you would like Hidden Chest and Pedestal checks to be placed. + """ + Choose where you would like Hidden Chest and Pedestal checks to be placed. Main Path includes the main 7 biomes you typically go through to get to the final boss. Side Path includes the Lukki Lair and Fungal Caverns. 9 biomes total. Main World includes the full world (excluding parallel worlds). 15 biomes total. - Note: The Collapsed Mines have been combined into the Mines as the biome is tiny.""" + Note: The Collapsed Mines have been combined into the Mines as the biome is tiny. + """ display_name = "Path Option" option_main_path = 1 option_side_path = 2 @@ -16,7 +18,9 @@ class PathOption(Choice): class HiddenChests(Range): - """Number of hidden chest checks added to the applicable biomes.""" + """ + Number of hidden chest checks added to the applicable biomes. + """ display_name = "Hidden Chests per Biome" range_start = 0 range_end = 20 @@ -24,7 +28,9 @@ class HiddenChests(Range): class PedestalChecks(Range): - """Number of checks that will spawn on pedestals in the applicable biomes.""" + """ + Number of checks that will spawn on pedestals in the applicable biomes. + """ display_name = "Pedestal Checks per Biome" range_start = 0 range_end = 20 @@ -32,15 +38,19 @@ class PedestalChecks(Range): class Traps(DefaultOnToggle): - """Whether negative effects on the Noita world are added to the item pool.""" + """ + Whether negative effects on the Noita world are added to the item pool. + """ display_name = "Traps" class OrbsAsChecks(Choice): - """Decides whether finding the orbs that naturally spawn in the world count as checks. + """ + Decides whether finding the orbs that naturally spawn in the world count as checks. The Main Path option includes only the Floating Island and Abyss Orb Room orbs. The Side Path option includes the Main Path, Magical Temple, Lukki Lair, and Lava Lake orbs. - The Main World option includes all 11 orbs.""" + The Main World option includes all 11 orbs. + """ display_name = "Orbs as Location Checks" option_no_orbs = 0 option_main_path = 1 @@ -50,10 +60,12 @@ class OrbsAsChecks(Choice): class BossesAsChecks(Choice): - """Makes bosses count as location checks. The boss only needs to die, you do not need the kill credit. + """ + Makes bosses count as location checks. The boss only needs to die, you do not need the kill credit. The Main Path option includes Gate Guardian, Suomuhauki, and Kolmisilmä. The Side Path option includes the Main Path bosses, Sauvojen Tuntija, and Ylialkemisti. - The All Bosses option includes all 15 bosses.""" + The All Bosses option includes all 15 bosses. + """ display_name = "Bosses as Location Checks" option_no_bosses = 0 option_main_path = 1 @@ -65,11 +77,13 @@ class BossesAsChecks(Choice): # Note: the Sampo is an item that is picked up to trigger the boss fight at the normal ending location. # The sampo is required for every ending (having orbs and bringing the sampo to a different spot changes the ending). class VictoryCondition(Choice): - """Greed is to get to the bottom, beat the boss, and win the game. + """ + Greed is to get to the bottom, beat the boss, and win the game. Pure is to get 11 orbs, grab the sampo, and bring it to the mountain altar. Peaceful is to get all 33 orbs, grab the sampo, and bring it to the mountain altar. Orbs will be added to the randomizer pool based on which victory condition you chose. - The base game orbs will not count towards these victory conditions.""" + The base game orbs will not count towards these victory conditions. + """ display_name = "Victory Condition" option_greed_ending = 0 option_pure_ending = 1 @@ -78,9 +92,11 @@ class VictoryCondition(Choice): class ExtraOrbs(Range): - """Add extra orbs to your item pool, to prevent you from needing to wait as long for the last orb you need for your victory condition. + """ + Add extra orbs to your item pool, to prevent you from needing to wait as long for the last orb you need for your victory condition. Extra orbs received past your victory condition's amount will be received as hearts instead. - Can be turned on for the Greed Ending goal, but will only really make it harder.""" + Can be turned on for the Greed Ending goal, but will only really make it harder. + """ display_name = "Extra Orbs" range_start = 0 range_end = 10 @@ -88,8 +104,10 @@ class ExtraOrbs(Range): class ShopPrice(Choice): - """Reduce the costs of Archipelago items in shops. - By default, the price of Archipelago items matches the price of wands at that shop.""" + """ + Reduce the costs of Archipelago items in shops. + By default, the price of Archipelago items matches the price of wands at that shop. + """ display_name = "Shop Price Reduction" option_full_price = 100 option_25_percent_off = 75 @@ -98,10 +116,17 @@ class ShopPrice(Choice): default = 100 +class NoitaDeathLink(DeathLink): + """ + When you die, everyone dies. Of course, the reverse is true too. + You can disable this in the in-game mod options. + """ + + @dataclass class NoitaOptions(PerGameCommonOptions): start_inventory_from_pool: StartInventoryPool - death_link: DeathLink + death_link: NoitaDeathLink bad_effects: Traps victory_condition: VictoryCondition path_option: PathOption From 893a157b23ab131b59b007dfc1f6ef0f28aba34e Mon Sep 17 00:00:00 2001 From: Star Rauchenberger Date: Wed, 22 May 2024 20:09:52 -0400 Subject: [PATCH 06/64] Lingo: Minor logic fixes (part 2) (#3250) * Lingo: Minor logic fixes (part 2) * Update the datafile * Renamed Fearless Mastery * Move Rhyme Room LEAP into upper room * Rename Artistic achievement location * Fix broken wondrous painting * Added a test for the Wondrous painting thing --- worlds/lingo/data/LL1.yaml | 13 +++++++++---- worlds/lingo/data/generated.dat | Bin 135088 -> 136017 bytes worlds/lingo/data/ids.yaml | 2 +- worlds/lingo/datatypes.py | 1 + worlds/lingo/locations.py | 2 +- worlds/lingo/utils/pickle_static_data.py | 9 ++++++--- worlds/lingo/utils/validate_config.rb | 9 ++++++++- 7 files changed, 26 insertions(+), 10 deletions(-) diff --git a/worlds/lingo/data/LL1.yaml b/worlds/lingo/data/LL1.yaml index c33cad393bba..4d6771a7350d 100644 --- a/worlds/lingo/data/LL1.yaml +++ b/worlds/lingo/data/LL1.yaml @@ -2052,6 +2052,7 @@ door: Rhyme Room Entrance Art Gallery: warp: True + Roof: True # by parkouring through the Bearer shortcut panels: RED: id: Color Arrow Room/Panel_red_afar @@ -2333,6 +2334,7 @@ # This is the MASTERY on the other side of THE FEARLESS. It can only be # accessed by jumping from the top of the tower. id: Master Room/Panel_mastery_mastery8 + location_name: The Fearless - MASTERY tag: midwhite hunt: True required_door: @@ -4098,6 +4100,7 @@ Number Hunt: room: Number Hunt door: Door to Directional Gallery + Roof: True # through ceiling of sunwarp panels: PEPPER: id: Backside Room/Panel_pepper_salt @@ -5390,6 +5393,7 @@ - The Artistic (Apple) - The Artistic (Lattice) check: True + location_name: The Artistic - Achievement achievement: The Artistic FINE: id: Ceiling Room/Panel_yellow_top_5 @@ -6046,7 +6050,7 @@ paintings: - id: symmetry_painting_a_5 orientation: east - - id: symmetry_painting_a_5 + - id: symmetry_painting_b_5 disable: True The Wondrous (Window): entrances: @@ -6814,9 +6818,6 @@ tag: syn rhyme subtag: bot link: rhyme FALL - LEAP: - id: Double Room/Panel_leap_leap - tag: midwhite doors: Exit: id: Double Room Area Doors/Door_room_exit @@ -7065,6 +7066,9 @@ tag: syn rhyme subtag: bot link: rhyme CREATIVE + LEAP: + id: Double Room/Panel_leap_leap + tag: midwhite doors: Door to Cross: id: Double Room Area Doors/Door_room_4a @@ -7272,6 +7276,7 @@ MASTERY: id: Master Room/Panel_mastery_mastery tag: midwhite + hunt: True required_door: room: Orange Tower Seventh Floor door: Mastery diff --git a/worlds/lingo/data/generated.dat b/worlds/lingo/data/generated.dat index 304109ca2840005f25efda7653198ff8ddb96092..6c8c925138aa5ac61edb22d194cfb8e9b9e4a492 100644 GIT binary patch literal 136017 zcmd?S4V+}xRUas;tE<1~Qy-%bmDp>uvzUmHeKS6YXy!<*OJebl|7U%!7i-Ps)ZbU6H`VRXW&>FlsGJKs6cnFGK}RX_%z9v4uj9jLpmwR%_D`!QCR z4i9d$d$Pv6I&-M(rOtFTLr4_&uPhxL^oH*rM$@b9E7#hE_0|jRVRTZXUTAar$U6)$WN} zUmw?cceB$jY#r=h=^YIEg?YmKx&4F7507l)#P#<6_RenH!dpefkHi(9s0|MLJ6GBT zYGC1DcQn?gF7FQ7do4y}ITq*QI`86jZth=cqd&d&=$h|r-)#3U-E8lI#4*Bh@V+9h z{7znZ@mjClZe89TJ#MoHHw+O34HlxA)txKg7R8gqZF-dF_P`4<3IcpNnp_=R4#1x1 z@9pdw;7gLim1u6M-MUgJ?O!Prd+io6kg0zbJ+-T^M$?PEgMPnv(7MtWz@0OlS~OX@ ziE(f3?g}Q1Cxg@EIf3D3r)D)Wb)(%sywtkdlFamUG~F2N9QFg~+z}$YsPn&!66kHU z zBIW&UYOp6b!ou))d*@o`FebTVs@-U|M6V0Q-R%Rf8M}mN=6W{}m}NV?D~07?}AqoSWL zIfyiN-VsLR7@xdT?}2(^ z`tl{o;@|Cnp19uHztW0ZRuz7)19jZyVA<;TJ3uG44|WfFW`UJZf6IgFK(?9%L_+yT*dC!#wpiUp*w(As`ZtZcsc^)Z#5{mE#S#K?BPcZxZ-&dc_cPH!w}gxV4NZZuq&*N34F>94!&~rvm21cs)8{ zbMz%C@PEOn$%T?4Frt_KT*a}pD{esroEM?7CRb<=0-uaBx zw%d70xZjLss)N1DklL#QaI0$Zvr%5G0e!0_w+6|)pNmc}?eyB)hdT%Rt=+;3cmyKH zCcMY@a%_LU^LZOZhw_p>f6)27M9W4s8i)LZ9?lilW0w!`DxTe9viz~r$nkAknh2mj zbAY^gAp80sp}^3*k;JlaKYg_a#p5!U+yebB?PEG$R6Vd14a22Sf%Xp}KDOIdcaA&a zkx=bUpuXt%VyCrt9Ta~9^bTl1Ho09@MGSR!h6HhB}pK^8OropnW$?`|r7hh2Pw z79dgDNRQ+8(2F_Hy^jjtWfxv+9RdgH_lqx?;wK!gkd414(0K9s^X?e4UDXc+&+>=>x$va08+so}56(C}rsh;Rfi(lX;>D#vOcrMl1IkHy8Oy)D3_EzzI12f=5#axCBugK&Y>POcO;g5aL$ zk{x{kz|rY?=jL8pQFL*qx9u^gS6Vjpjb7Fj8v zvgEf=7vF%(t_{jd80YS|}BwflYeaQ=21E;w-2?hgXgRAvF`@o|CN)yw^!Y+i67xq9Y)v$inq zM_X=n&NzaMNiU~^Hg0@N?>3RJL3F|F-I@8?CPlY91a&)$7-KnBj?Ec(J8z=nv_&k` zg$YR5f<-lAJYQvx=QOb?G?eGaFngvN9hbjh-D9e0_Y>CeI`%&*W?q@*>_YdP-xugq z#ozhb0H#x5xPR?o(e9nr=Wy$W7!gN1ri*3u1XZl(Og)?3*x9|-+u4(5Jz@y!MRb?8 zoEHucgk4!tPLNmkm!Ub+Xm7)`Evt+1bVe&MepBl>Whf+((wJ+B^i#CN24D>uot3z0 z3n3yAM=-G}#dfyYR}6c#-NGmqn}POaA#VGn5~qzVIGx9cFPrWC*7gq01q6$niXYQU z3Y?aKmoSf19p>dnB7~4Nr0lrF8dBL8d)6=S^!kU&q6kVRe~j+nToaqx0}Z4KtAt%7 znojtVypx@B2$k7d4Hrt?jGNZ>Tm{Vsui6)KuI?g+0?k`s;< zVFWHzTTpO?2tu!?)(#*n#8Zgaii4c##Mq6Mj|an=_U62h7{zS6Vs?~G*y7kanTt+r z9rUjB3ymB2Cm_!mHf{%Fl$`1r)+s2lv%3V`P1Bu=aa%4DptAcaxlvbgnHHiso15bH zY~GK>I4op(l}eM9-CtAulX)?4XvrTlwrMX6oI{enWUY8^2?xJd;-dkO-gz^+B2IC8 z#ST#1U1Cmb!Ic243LW)^o@3y8Utwac_+{dTq|A?12I`KDQmAS**Vo#utA*uudp9c{ z$D9%|7CwSN#DAHLOPWAPBTeVo7zfT}js)Z~42Wf#+s<5~>78x5$XUo)7A51yw29_& zBDFcauuJ@3i7k zO<)XDN@A2>#Q7rmRDfhB)?k^1Vap-4UU*G2F1<%J3f4(LXVfs=3Nvv$Nv_8%L>3g; z>jZ34@XW}FeE5i@nRXfsVO~DQHfa^gRJmgJEb5dY6bK!Rx#R5#XXwg4oPBz6lmAszIe3x$hZS_DjR$HZd+ym%t&^2;x-*z z476l-?w{pOBHP(Zta#F!D`~~EjTK@0B|~yAM$+#U$m74%-egJbTQb+2h@-BAFUb`$ zG~ceb;x;{uHI;K5-}%KHm$HYqNycWJSxLBTu4cEs+yPr??5Mg$2a`QvjE%w{!aGpS z+>%-BeXTVGLE~Ag?`cz1))sX25K2O$wR;HZe*HRZxk+Q4EMB7&G-HlSru!}Q#IB4> z2*0vq0V1iAWXm!Wm$Bv`?$1fv&`rmqX!{Q9Ed)BeYTyE$xQ|bv+|hetS7up3{K{sn z@0MVdbnzTEQHQaXMN?;OQ_*{{FE%GfCl;`%ftecQxUG#R{>$hjH>nDXj9go66~Q`( z((Wh3xcoz!CC56CH3KUPM-9ErMqDoluo3?pU?a1ejHc*ZS{{N$Gjfwzh#f;5WA4eVHhk7$Iu&EC zL_jr~<*!3?Jwb5*-qs}MB2}-MOisTyrmIZ~K4p$h3invwXe6xZ4{GhjZkbcrZ+Xu4 zTDFFPX*-gW;=ZLhKD)XlpN?psKZ@%{R}01MZR|pr;s1m-{F~lI#}?Wh_&ybi1tEux z#^u<&8PaE{>*N6|_N(w(E;Qag!0Nz`T#^gJ6nub7Sxvam9v<*hnv^1|2fdvagm+~L zx2Ce7czOY@_AQ(l&^Ah0OtO?aNmu|cSTI{~et`pAhEkm$%fumRgBwk(;}QdT>IzQL z^4Ac(^P!kYTT=pEn^9PArD?US2U6~s-4F$2}ZAELBYC0S0{7}wiZj)ZTh zcoMcTHC$}fhYZ->X=5vIkQ@o&gc@R;LmdKrL$5R7d)mf#0XH&_gl({VjB7$)fNsK< z3>Z5lHwHc7Do)#sG-hHfoKQoIb+AvsIru)cQOUthd(s+kBq}G=5M%6oH#BkStiNQy z+L^H8a?FvC4p)$bv!RNMv;J~)oEPYR=gYxaRS@GG?vgLv6tOa3?C{h|uZ0^MN8sgP z`6PT}dU*0p5-J0}w&P@WL_0qfBNas1pI(v_8Bj`Dkpr8X7>$5ta}T$2*ntKt{Y_45 zV}5irF~^Xo?%I29G@}h#N_=p8r8yN!Z&aH#U8oUOZ)@-7mi4z)dPgS_)jLqPoV~$v zwsy%MNAE$8;a&+Y!qKH@DEND1*mGZ*O478N&PQUlu>9IwZP-4i$auskiDUPZ?=npz zW{v86JVwZ{8O^rL?kum2-P6X}G2n75v_Fxj5{& zcm`>Y8mued{2sOAq!7?UvN<#{E3cWgN`6`4QbG%iye}n~SxW?;&Cv`0B}VXIm%`D6 zF02@#(fLf~5p)LGUw738QW53ox!>+BeqNaMQImIX!{0CU5Fk5rEV^}P)YZr zUAUx4YG3%Wc#8z5^&+6FCNXL7eRMC^gJ~kL&`W{JZ-Il&OqUB3Ye2JOl1v3tCVh%d zn|ekw=x1XFO{y=gsavFYHM);<2)fS{3g{4^uB*|eA$7_q#NqjA=-&B!+zEJycid#(&C3$)kG}5Q9RY1#61x`fB8au^h(V_VlBlZsG5MG>}o={ zhzs~NaK4wY^*|A4LOD##mvFGd4<2M2!V0TFsuZhc1We;#5Q4Fq+CuCRzZ!Qu&D53w zF*Ftu(U@?j#Dzc=L@c2fz5@uc?1|V(#5#hsX_SIhh&D$;D{I|~rg7c!-jnWf!l;D% ze9H7$uZbb$F$J^0NXCfSV-X(aFBM*Xy)jxA%94FBNdj-Pv4l15YHUzn=yjBl-WD#B zZZ3xMZPp6yCCavQ;KWmpy|{)<_tB2X1ol8XGm>vL&4cCI#> zgUjmV0nTqdT(!j(PsfsR%89*RjlA1q1Vxr z^r7^oP|`fu*d6RKG4b-5*Xlo=>w^hmst`BElfwGACc_H?t>HoPIu2zOO#OKw-?+fiM$2Ai#V znXr}AvL==hYj77Ir88Pxj0IW-E@Na0b9qdtJU7-fqpMu88#9!+AaG0w`6sHsxG>gW+TI6$VDKaWJFP=8|@1(j6)B|-?$1}JsQ zW>Lrt2g%8i3~Uie?&&g39L5dS?9t0o+)t|kJqn|S&Z^Q($PDu;ZSX8qlyFHQ5bGiz zc5Mi@l;ZHa{i6T})HUnFqsVJx5t$;3+a$*9C=&YmQu5_aCs}=A~}A zDXwca+Se#lJyY=m9y+E{*=|Z7(VLX7(G&!7n|X5E$Miuw*F>PD7*8=P)Kj3MQN4C* zi-#mbuY=e*R7Cf)fYD`JLH9rpUM`6ztL+ZiiJ>`t zWG-AVLbi9Ota8F2b|T|}sZ@Q4j_DfZE#**^PDp9guO>y4H}MYu6T+=2_8>_F9BW>yq0?M>G#QEdevXdAtlYZ{~xGFUXM8KQ!$(fR~{L+LHll#UQH z_ojSKt=({c_v^VpPpc5!Nc(JvEQcf19m{Br`mASb?9zPIEFv3&MfUA!S~udM^z>*i zRS(A_Gropb8NSIB3vnAswAhZ7$T}Q5Xy`K2mRQyFCSJa@$B8kAyDA3;W>2`+`;P6_^TL7A zrm5_(E%cn5%ePZ~Qk0eNl$1MZDi>A3yH;l5~ zE^9-~)oS&|rG<+kdEW4y$elNbKM~8aMalF8KJQ0Z_}k`?v*#Ww-bUe(f=S5*C;r10 z(%K@v9EEwnx79M(fwJt;b8-I)jKl3+WZA1mx8oBjKP@C^e0GS`vw)hUmUJFIk&mq^ zl9+l~+q$;{llkr`!sj(^o5J&srFg>?E)T1vrBam`bpkb1qx@=FyioYO`0Vhd>Wkgu z^i8zS#dEwADID%693FIAV46c*ph^rt4a%$5ads3$Q(G%tD6JLdRrD&M69xf~*Ncm# z#bP}J=v$@DptDFUU=G2==&Q~?O4#4W5=vo+EkrNOdj~j_j*{IYuICJI1BWEqyUUYA9yR!qIfAThv^F`O#AC6?Z4Z`QOcDrm? zbxp(n*O@Mv{Mp#C`=^lE?;P?Jd}>HKM1s53W?0D^^7xQP{qC2QgM%xDwZZmtnD%lS z8W|*TtVUyNwJpJ??U%$v;WD7#S)pkW%*>_md7kk3GM)bj3=+!d2OG=avX+ju`ckQG z#L&2W_E|+rp;NKMFs!GVem6D!YF2XqG`fR*Z>$wJS5}+6^(pg?VG!M`S*fp6JH~Y@ zu`!yFtvj?Vm7u~tlDur_!f$agaEo?L5`~O*@|k0P!W0!OVTn06ot;AE1BA*MMkqV! zT`b+~t<~gGskl@qRhNz^&Q(1>De5s1(8Kk77*MJP z6UIo@@J~by=Jm)LOpFRu!!L*$%A*rZhfkJ(K6J3b)@S-0sz_7ft?0T3P98O7!aR$BpvZO1->p z@SD(Y40cc_nMTunP7o<@m0za!RTzDNFnXPCR8y;iyW!hN_hjd_HR&Q1TywQd$*0vy zk`s?i7}-lnjV4oyIKS-}FnJAIJ{~a{UybaCnokz--a!k=389BUF}!F{7?;lshgU?D z2aoa<0cQzqan8xk`?=UUX(3#N zr9!>N_&%>*Fr1?MQ|hzVmtcT?h1vsz8dnc6zQU(dUK-K?d!oDS);8=pOq!RGr3~3XLuN z%epg8&kfU`D#9@d5RUN)R+G1VLF|j0?ZOtqjAkG`&<@Y82jBD6W;vsMz)qZg`j~)|&)fm-{`*lXmsidBn_8sZ>XS ztvYs0MB~w1d~B^$tXB!A&1$10_4QCY+yofSF4c=$Ybb*$z}U-4jW%0{8b>vY^-r*z zB)+|xd{C|?)@s%#S|c@}%4Hj`VfGBhhP+9OH03zfGk8+G)U#4?fO?7ZYR32+i+=Z~ z16X{MV0GM=U`cjkRPOWM7uQSGHx;V*hvAc>w`MHhH6;KN@%W5VK*I4Z6)V1v5POI$ zit*+Fcv)(`(*B2$$X>?5Hd?z#kkCWw3rcXH>6T%zHX3xlsMv^Gc|Jt_ zcy;6E9}K*EWBhaQ$-b6c@W1VUn7esP~b> z=(S0}>%~T+xL7aInyy6IlzB%O!`v%ukgtSUfoHE(-0{2Amj{y26ZTbQ9(r{m&{}cv zyup8bK|CJ~cPS}C=};A(Um!fCA1_ilv|E>V+a#!C%`XwVMJyLgYaW-+47m=9?frT);E-L(^9I|X%(w>0K2Se(bDY)ofZs| zej;Fn)fquP9@rB@>tzj2##@7mvl71KjaO^+<|>y2`K@x5jCe#_tvW1~i|e(jtwraS z%XO*1Y11a#ANRX^Qi7%INZ&+#dcaClzkXs zYJ5s?jPRdl^UhL4+DLJPGp!feaF=lb7cR27H%3pqA4vws4A{R(F%8AEU1lv#=Z-ru=Q1lMN7+TsQMCTHUVHsw^bQs-r4Ka<{ z`>|lSbVz$+WnK!3hfwBOh0(Kwkr1>dJ1wJ`MneWSHFSj==SuMzHM~kJ95*#^U1j9x zMsql(X&2xlSZKiDq=nZGVRkR04NglfxYUnE$C||9Whvg)YbhXKstttw5>BeIys$$^ z#_K-1PZbC_tv0d)(LwiPa<>~xX11)R} zUU&hyoFPAAowBh}v^OZtQgPjM2|Jc%sbsyFDz9%8o8=NrCvnel+*2ybXKe~O7URpu zSFGjz?k8lAEhjRgQ%k$g#$z)xYuO z&1&@`@7wsgSc6$_bB*G%RS8~TOzsx zSL(lF;Q9CE+9uAnAA1-&u=oZ3H3L7XzjCD2OM9n`qMy{gPUI4co9Auz@mWI@3HBwYEKoO?FVnF*hlVjBi=({N;*-zfY~(`TguEhsLG+ zaJ*wvTNkq}v$0DZXIr@1zP<|CKM#@0ati)`09)BNdnha!<(cL}{j<`hs_xC62#f`{ z#{Fm6J4uf;^Lfbd7^Dez?o%{*AVd=m_x~Z9ggA>2f4~?UBBPVQRT1(liIDeuVg~XL zxklyc`u}fIO6t$lZtTV<5OBWBWY>OB$@f%C?$^=)yu}&-CPPWJV+W?%OMxs#y%uGE zlE++5i4rVE%wCXh$QOKJeJHKGYSCWi{ebm~g|1Q$-k4TjYZqpYwg?6z^^_w)>y-w2 zXrs8sG)}q(-D_`C2-eoFzRiQsR2!v@4Yh~Y3-OV=JLn8(X`X?@t~Oa{1?PF{@9VAp zGC_|K<7F^;BnR=J@NnRRMNbSzMhl7MxE@=z)Bt=FA@t@nggQ5o=2}>G4 zRq{VC=gH`p>Xu`u8d8_&qlOT2F^#|6mQ=5O6zw%NYgk@My<&Hv(%~hrJkc`rMX%vz`m|cfFX7H zm`ZF7!!2B|<89AJ{gJdXrh`&W!N{#iGo*D*V?v_CHluKK!7fk+;w#Lnh=S;iK%2we zX^q7~t1mobhaph=+I9$nC+Ui2RAaW4#7n&%swX+udH}i_co!vL!c8-go0L6N4;%}u zEbJkeVA}N%pt}(a4998h3B`P~f`bvr*={$fElAAfJm`Fg7(Mw6V)`Ul-aCVUh6hui zIfF2(LRzx&aah0M&_?r6O3XB1(mr55&j>vZM=yA=WUQJD7|MVTl~;JtT@^htw$3M| z7hTZIIk$onn0D_nTSK#urK}Wofe1h9a3Ok7^2-8sC?O zOf7^AKiG7U5#JaxAdPlZ)|81+4x__0>8^%IT-{)($d3k_aqK~q6*&_+Kc9w9pU@G94y;9rj;xRo=St2e zg9@y>M_`sur6KaJ5M9){s2n1fF7l&6X1uXcs~rKI-%3O0+d}B5RX>c5{AkdbSSvN5 z?MQ!(Oat|WG=x4t2#F&#HX2fVN_^bOB)%DR;5@Rnb_8Vdc_~1HV&-p$kP(@{6U2=8 zW{}C1>qi->yVG#_al*yrU`q6zz`^v%U@}=-Xf%t9`dB@Sn0Z2Clakn53FCiEm_1^t z(c%)BZF#Wn;|(KA>qnG9Tsk9v-?hcsda1Br`FqS-uh|A93hD0bP^{ni2-e0C=g%J{ z>>py*8Q@`AOxWr39S#^AmoBn>Ql8wD@Peb5bOJ5UusR&owuNY0}HwtdgB>JpoFvYKpIu17`r`w8=VkzaK0IxLNE=)=ATuVm))5IyW6 zE;a*iDoEB8L!jKEOeZ+s%0b|#jBGn0KUz@?m|p?+w^DtyBtYPN^v%>E%csxcbaO~* zc7rFRlrX2MxQsO7ptSoha4bjsFY$La$lYI4U+trck+Wma{aLxNB_>Q)qxRL*$=4Iw z?82X#J}PHAa}N}12?Jau6QNmu$K^Ad%`g8JV88Q8T0NYc(hB=^!d_^#BIPt^wtKkz zQC~Odt`Bog%BvR)VZ{_937ld=c$$#%Ob8q8{q3Dy%Y-0C9Ggath1{N>xGtdM?fF|% zcIU^Hi7OVpiZy{Dx7X6;pBivUlE{K zz1)DMR@uj3tCi2V=-5N36Ffgd_Z%8fj*U1wW|uJ*9K;Q>i}O!osL6V!N~xz1Wo}Og zozKvO31+%ho&I*}wAg-_OEi|Gwia&-#+pB4^z_P3p6?;lUTvw-z)UDx0UthtfS5P9 z+;|zg-Mtob;(Ht(v+&+MxY3Vi)8inU=4JP;=rO5P#BC{x{V<{R2F?N&Rh&Xv1H(}q z8H6N>9~X2di5V5!_z0}2>EaD=qs_lf3K%{G6-SS{Y!u2W$gtjIHJC_YAr zJd{WY*$1Kf;fzzEW$>A5uEGH6Vx-@4<2W*_C|KpVsMQ9x_kx`e_Rp9`pCQz~ED<#c z8*2-u0d+M}tp~cEaz78Svnw64HE+jMleE)+sY4g6nHKMHm~0_p7S`vDe{K2xeQZ6zljZ$W$w zI-ey8dmou<=zezScwONgMTL>;0(S5 zz~Bom;e)T6lp~m=OHVEX&YnVv{=5p7^KybeLa5g85wl&VWT(hgnS^AOQ`KRyymDq# zL-ZI?=ni{`6a;Po3tAcy#h#avLROeFk*hayAi_PEfCHyPz%)RMdP4QFbGN&V3j4PP zQ3;g|OzQm0muO!TOSFvuaTEkjM9BY>3;Xc~;z=<@C1IpJus|}HXiC)O5Gf7NCXyYN zNyK%~H75I^$NM{M+d|D-6CwbtP)H^4cy(vrOwWE`dP;yek&0oy@sjOaokU(2EEIm< ztIHMV9stia5P4s@=Z0JlJPpdyvJD$=WC5`-r&K2Hsu`V)#f6 ziXnBbTIR0r36yrBaGcJsKx5H!s<}g*4aFyvYUy;ql1SlJ7Z5)#NGQ^%hn8&^Vty-r z4;=dht14}92A$8p6D#Ju+~iC)2Qwae%zlB!?9tFlXB(`Y($Q~q;9QzY(o0%Pr6o#-=pS*RYI*#+$te0 zIqg;tn{@aAm6w)qloM}T^6G`(=xA$A*mdBK2^n#G8zU3XOm+?zf5atncs{YnX%qSl z!zuL=$Xuxfqo_-iSKZ;^VZ%vZrWlV^+-_%M0ka!DD;zv~K=*TMFpgCbQ>f#$K1;2O zD>15pd1{<44>|BMk4ucj217#Tla!wzG{mO_e4-N#x^702Pe))>n%Lchh=|tUBI1>> zMr~f;NeJVLmI=;g5lz7iq0$MM1mp(YKNZA+x#(aJpw)wfqPRw-Q8aDa1){qZgu(rh z!bS~O{g+**KkPF zlkys_tO?ai<7#P?5)ij$8U`>C=Tjqb{tCibn4mSzTR1ro!x=#N8$@6k<`tP6+%O=m)`_delze76w6s`i9*IY8p|(X)D-?-X zALe5^3XlX29dvIh^4JM%l}y?ZxKXW^wHfotN@zXWWKC}QWWi2A!^+LA+V01r;;$*C(AqjFV2RH5x6G@Eo|6tB7`7#ob+DyqCNsE^GSG z!)S&>Cm=cKepm{^X@-Cr=kF$D#iHWZ4qRA)2={pnCt|m8*tsc1B^I^va@mAWl#^B0 z7ehi`#E=P?_Pd{!m`;%{Bx52fM1P176`F#s*ra3zq6R*?pW|r(k&{`h(s7qFUvACH z2)qG`{g^krnyFHV1=BWhW)YDq7FUbw3r3N%Cr6Qt;7F)6!yyxibwY&+Hl0_*`Y9jB z_TIrhpPnvDl2_XKqPYRA0!cO>`rj5P>(ggEOzveF|vuEaT+w1<7^@ws1`(2;c_QucBT*7DCld4i8kV;?c92 z%24>r!u;8XvlC>YBpAn#1hHmi9Zc$HqDo#>l|Yw%w=C&mMc-T_dYrNPrs4uh)%?ED zrzWAuBh^jlH)=wPT@yQG?u#_>bpM6Mg7$7A)wSNcZ4nZ$qa_jv_;&ipghQ7HcXr!i zd9jN~i5A0Qm;|68xUAHW61z`s0*4lYh;%A8k6<@MveEZ*)iYd%iu_g*Aj+`EP`?yJ zu3oG08&SfcEXHSE-~iQYs&o+tj8H{*bddzuSpmCnaCmsImjQedow&GGKF`pK5^9E* zy}scn=T`z;4n7{zYLgDOq}`@d!KSLTCu(BXV+fasp@0=|%KVNieVPks=&hsSewXsi zgOR*!?MkZs4Pqr>iHR#wLxLHtIZ0=DmwS;I5XLP;nraDF(jbPJLIaA-L*b#TVqfYF z68O&<4(zCH!sCTaVt7~th>4mE+2l|pD zh}O|#@?($ZPjd75dTFCr<`)7J5y&kR7x?wpWWcrB0_VX*z&z|LC3PJo6C2(y*)W!8 zGHJbH!cPU5P%#v(ftn3v8IK7Sq+mk{VwlhwOt6bRXz)z(21l@+H3)Fnbcv0cY@*_Y zO*|B0L<-J-fh}Yo4AfZH*`Pa8N5f6lOE|S^lu{&*2F|SE6q5WUPT6AltaSn?fcC;JwBzKYFl{A62~ z`jkr#Cl;PJ@7ZK;-hQ_p81z1&F1FB&dQxE*XTj_(-!8`^9@<1hXOB)gZpN3MBIjP^ zg68-c_q*Ex%-$_VepTMPkb!6rln^3GVt0@Ag`7BhI3J-M2ip|p=xV5u&JYymOC@Fm zqm`(Lb33L2knF$;E)@+umrhxY1$4>pk$h~;r|$}M0Q))JfqShwX_L4OM(}%3MyTmn z=>c{KHjU8O?9J_5Du-rzDs`@&d8yE9=9W_&ZY;OR30}o zzSJY+G)Abgs%~Cq#{NSA?0+_Z{W*iZOMDd)N1?A!8Q)r^An;6PAju%-N684*?(|av z7aTA8>FAY8_i=$4ZBb(oF43$jAq--xSVf+?H7hC9G<(Ug)PB$-a~mck?fDhL@O7}@ z^y$di!PH6L6N@zA;jHnc^8#ja6S!GE+TsLs(0xwK`-Etgzd>kNsovvB5(k-6BQ;i1 zS3uw$1cNXxeDt^5`|aLMBQ(!mMks0j?Y0q^@>KRTkBg=Q->RYeJ;La<&<=FqFihqU zcorC=C&kFC&ddlC2r}+8S1q}eTZXefpZH_?Iz=G1OE5y`Ul9KH*!Z(BWN|!bo9gpC zGA?v-IJBtvDaq7+JV@=wJ!%JIb57sw54@V7_Q2bHnP*)Anq$SPBwsR?=Ym+C^RNs> z*u0C7bZE()1>=cfaqv9v2aud@P`0B-3Y$vEOYgQ2|2`4K^$8DGYYBRQkbF#CiPmBN zuIea_X!mOdtt-93btFD0Bl%meFC*h&rr1m(b7IDeejrDtTG>a5oI!VDR%m<^&eVD{ zVfqkn)n+^3s!K!@|%&rCEx`Gel{^~I97yL1>*p(yii~)xW zV4s8MDchPu?Giqw)~j*KjAp%YCx`0WxR8&$_xVTPdiLBCPl;E-4$q4Y&qwhNxQXkJ z?L)l-zAie>GWY8JA$qJ$EUE;i_563e>Nc!f_)j5}Z!E5sxP=n7lVzNj7aP_XO&~jA zvxXx&Hg0(qW>eH=1#O&P-05xawn^qL*b%OHGHe3{s#0JU(?CLP6Bo)g@@g9ih`o7J zE;;JTb$Z(B=pLI|h55CEZKR{eLBA&T`-#+V$wF#gIC^8agc1ql1x8V~)EJq${|b#v zfTsrCuac95pj@PQ%iPNn>wl5^Bx)2@@Gz0vpJy zu7r}5)6?cRX#}iEfCM$y5ofmX_5nP^iNW;LQw5{fvj*dVuu8e(k;iWNyb3Za2!Y_1 z6hx@LxkzVpOl*xe&X>8Z6}!#`tlo?ZZMrCSd8Osp6!ww}aE!OFS+RZ_(Kv;8f{P_& zP?Y`1G%d^-qnO2dEXL1ZEWVt#Vo}u^3(5^Uj9$lpRYvl+SY_Oi7)@WA<)v!X8J0!( z2QHX_iozFOmP=N;(l8KWN3oSETvL9zj(i@BGs0jwy2~%H%0~8B3fZgl!$kLNb3hez z=74;gPWcQ}r90d8+U5e!#)-qVrOl1C@*)mN8U3wgS~gFM`Br(26D1EFnGPf z*}3k35=q2cz*}ADhehLI1!D)p z$%#ibO3ZVAa- z5K8QtH(DR1(UPv_e6(EX3?OEtrWVSzm3nc*Ht^*ai;K-o3+-u~wl_-)b!%CH23)5E zl#B$bd-m{*FV{D5O_71+*Nf{o=I5_GCEOgp2C1|GC;Of4zRS~}4^zp74p7MjF;toW zi7u4Zm>PodU#OQZl$+K&bHh$C4Z7*>BPOBbFE;DtMc((xVzajHPL_#kX%oj57LgR7 zP2T)Q9a16$XU_*_Ez|hbHk#!{InV(j54yiEMO_?RpLBb@0^NBJ`0>>y)`wb#bTCc< zij3nO98|O^+lT%x18thcVM4{&6a-PnwV8$S03rK|RrtAFTDW)#ZfxtrcMj(}bNCb8 zzFHo3rZJT%)cx@N!}({?CjAel`IlajM)o`s+mY7ZeH}Uz1JHK`kZWGtD5ZUdEhYK$ z_*T6PJuJC)k##NU^Psax{g_-r!j=N5w>BWoF=8ymDq(--oxp{nWGn~W-%%@tbylKl zi)N6B5E&TdfnX~p;fsnwljL3~cvqK@Aguk#bqL{1AYkmoTE^h3ll&ElBUAbqO|RD$ zaK4(3L=2RMMt-T(fGkYL1nx?WbcnG=T2IC(k$oZ+Zg19-KaMqPDPKo%<5+1cjaScC zYbm|XSBsmJ=r<8@#V0AG#Wi2Wh%|K*D;lHxCy8HiKd466!3#|oIDM!W<-AAaXbAMe zSqFk7gM=7#R%q@ik!jZxjbGlSBqQXJu4mv>V@>oQMo$NLtk=F`fx>J|_n~btR@E35 zS?6oq;t}S&2>fIk>6}T zZc&y2yD|nbNwJqxD;ado)2PUxR;Hd??3;>VV%1~CHjAUZ1Sm7;Ub~$RkZ>VmRp+g7 zx)$Ycilo-o=ud2v*H-G~br&{AG4h6VM5tLR&cb0KXy-Mwvb0b>4#Wt+X|r5P?V-dW zw?a4vpEP$lh;r8`Xt%}EowfQSRt+4Ef5%qSxTN>ZxrAQSbW+o5SZfmr*OLS z_&Eo1KFS}6Y-TX0h#|yw68}jtb9}v4s~1Y^IJT^o*Kn`MK`YS8+$!zXiR#cDT`4gi z3272&X_87IaIY)#f;@PP?DBBpD3l?!JiP)vQsXOI;~kn=2GJS?I=6H=h!q>ZjL44r z;wFot#Gq3X;&iFB2y3201y4feC?c#26`D*hYX&1Q0CB4)-GU8~8Qtx1khZWt12hZr zGTx0ti%{c`oH(2Jmc(H5lavGs4*VR~&KZO;T|L-Ea+x>{U@Xy0Eo^PcFV^Z+YucsO z!8ks@S`+!nOt-R@%r&?uki;6eANwNOX8=1gKQSEaGKtw{bTS zvBJWdK2Ay)yekUXi_A4@7@DILP)LdFKrT{2Tt-1^6h^@XcXBk+`j(H6F+5sPrjH0!t|qH>X0zjx5Of>W(=xvH42R-`r-Zhi{> z`;8)U`|v}Lxh2Frx)KXU)J6$Xg$C6YwgR=$9dy5_r<(9JHxon5pukc-hB#CX?*}+> z(4AJf({Zt=4#$(x6^~Q04|fj~tolAxHLjN;z0&BKbIastIudTsc~)q?hMrClZ%L1HimV9ijcy1P5NQk}TKK#FC^NeV$6DGVlTuA6|W zylmRuOvHmtsmkhE`c^2hDqLj2B~pV?5=-MM-!mSZRu|dkVbEy_eNTo|;`OS;>wzGN z;|<&99)(~o;&K(UB!^(ewva!V=!NMUjbEcIdDc$F#AecX!TWKrgB+z4{Y(y6xFjsn zxDX$ga-uFO(^#lU9fB$@njG6Pxt2k9Pt0a5p>$<5R>0Sta{cZ%s>vT*43c64$L%W{ z2R5YI(l@qn(ez%m8koVjhzB!N1}PHdmmtP0H8YX#giLmN9B6xLsk{uY0&*!E4ID5e z1%zlcUR0?Jx78Ky6@8q z$5L&}Q!4{_PLco#X}HO-mfT7jcvUYBGDae8Cw4X95Q(>JG+}jG=)SBv5$vhC8l+V~ zSV>r`aY?I0*Z{2(VbA$=O@w{o2(WKG0_@w40Q+PXSO_4PDLnlsw3j5k$hFFtnL6mS zX%!2~Pc8oxxtN&Dt@vxC*;@~~AJbC{yQ3^qE7zo4l!l*^rV8NTTQeGNs=N-Sg0U`> zj4WjlX@l-3bf>D6#$q%Q2u+j)*YV}eDsE4Vu5Wx@#Ho zaD(ozrLD%AoBTyL2ZbjK0j3(rUPu<(EE@=@akyuYfmkAP{7AS#_qWqKN6K*?*HUpc z(wL@WMTyOwNjkO2DjFqhHql5&3k_SxC8V4P#33zPqz-UN5D=0Qn@qwtPQNm`+BhrA zXi#XU0X4tr;m^kiII%UX0XXzP&aFhSLV`mgzQ?-G8djIiCzDerIcna zohxmwEwIUwx!_E7lj0e|VuVyOw?Hvd{$&XsoC z6a!0b*CL*HP~{6GPZQexiIvhEtm%EsM6obm+sADdGbS98F*`vbby5b%DT3@^ubBaM z60menDJv*;?HND`S0qNs0*T~@8T${&jQuCbto;XU*8US{#{L5|TmM<|$59+G=)9d+ zQSlW;2p!EdGa?mBVYzp(e>fUCM~Ov7f(^P4=HzK}UR~6(F=sqGy@WG~!W#>?V&GP* zPb&E-GxVIb42s~OGay2UpdzMe5`{B$-2Tv6ZbJ*`%u)$Qjc+V$VOyR7qe1uER96u; z&Xf6KqJ}hi1#Af%0>&wXI@<`GnjVw4|A5?P)%UuIPj)%IrsLCoPtnp1F*{f^S+1Wi zASSopbA95%wg#PVcqc4YGEodCvUBdDvjGK&-S51chH17@eg?bo#vXj{e1YJd^dn?E zfiLV_%K$t}Mw>N|45Q+7C{h;e(qxY^uXcd5$(C~y;E6d8IF zS1^ECgy^awY|o0d)g+n#0)WF;Bf9kC=+7>`(z?cI-9lI`TPl%$Y8X0zs%@H|Hb zoevTdPKcvu&%J;=T%?Z`(o^8D8fOS}Rn#6I0#ZH{K{puq--caXWH7d6WGmTi_xrdl zAgjMPJcL1b(7kh9O3`4YFT+eU1v6c_zDTzm?2Z^l8+7j%jRSMhW4@_-Ip(B(_rWhw zy4%Yn~ep`;I#wn}}=Tbld<8y5B6hNhMr1gJ5^J z-+iBgLxmnff#ZaJ_XlJFcvkvHvi>2?hg9`Awf3!m0ga>I{drkJ1pl_t_Ia$|{gkS| zu~7@S&H0Xg{qAqc5^$lVyA8=IfFWkT`vuhs-8=J2_-AzeZeAuz&z0*wRz4f|yLU_Y zNxCLnT=Z36Z;|SEehSJ~qG?whQ;P72BH`B6!ERw*`6)y!3%Br^kns@>obrzV8#_mu zB4dw%gX&DzFa;-&HYY0+Ap#B1Md3tB7s(|L7(j6=&Hz$qdVs}I--ETuBWkKmj<~7K zX;ubn0bo4P2Gk*o5J^;}9Nt8@1rk4ilcQ?DoPMmp6BSTqED6%`#prjR5aSI^S6v># zg+tH1z%ZbY7dVzoobIq9P&BhGx*zP!8Ey0BdiqC57Uszd1S=$knh%g2DWFNAi zrE!*cJazHQ*AVFq@p5>ZaDSBCoLX`M5iM#IRMr$FSs#1D@?|qudpfY0^`#{xb_sj_ z`9dt3UdCPq39oE(A>n`pQ7+wjLE%2RaWQL<1?rl28Am9DGQm!4;GEh&CrZU;ys?gZ zHVzG0`x7)eUkc%`76j+lcDi(a;k`Cjmn-umcL!Dy_ftlbB z<)HhI6klghz!O$Q4<$k;Od<$kvrsx}(ggm*S>cV-mLy*(duuLE4 zwrVZ;gYM6!O>O2esyvw3TSuI;L=h31ihFZUra8Oe`T32cmu@LyWC@moONf`IYj@N~#E7T-{te zp9Ya{RLe~SH&4WoMZrmg(b1!l0Y=&WV7P)OZ({jZ<{H_c^GTr%ZqlU{Z_>LPI=yvB zM9MjC_H?yzoN*vic}UrN89Q&4eI(A#-I1X;80Q~UeQNEJX5 zx*no>_-1E$8;-Rw_KZ(lb+pH!8vb_ChaziToxF6oG$-TAF$$f1t@Q#@CyDFpAu203 zRhv}7MQB_)t2&S#M+kpa2gt*D(0xed3r-a8&}?MgO!9%n{R>VW2Kj2KwB*fa&!ZIg zF8zzE0jNfKW%N9l7FRnEM4{+8qWJjHZ8dKu`z?!`@@o|E!y%_v<9K5e7ZFE-!GsN2 znt?J%jZ$5Lh0s%Yyq4J=gi%ONLfzOv#iP3;6RNBinRhbj|1n3f88W_J+w|;SJ}Hp} z&2ymh`jT7qZx4D^4LY9@^3l5(M))zrrl3+|ha6I@1BXgu0a@!!8Yl(WZ-XJ+$n&Yo?Msl)~Tsy+P#BhDLUqxr&+fG4pJnZ zbMQq7FMREkCGwni+W$}vAEMxef2C1CgSo6Uu8Irw1tW%!R%0hgau*N$zqx;JySr? zM1^@=*jJerAf{!CIW=Gm#XJ`NKEa6@uzvy}^7sjY4?3S0OXWssW5XZcfCLX-A@@gO zT)4TgehPjGtwEvNX(rmbT;TjwNR@IXs#;6H2G}_Jg(e#1ryWzLw}_;Soh)uOd6eRT zp+ACIJ-`QU=_4_PQ#hGJm|e!6SZwi}dh+peXPgS1y}`=(G8&zgfgyL`sc*{oE@Wdb0aI8+(Ko6kU8P~sIQ=M zYdGr)%w1Hm+kQY|3_4#F;=A-T0{q$NvifqXo8G+y_Sx|&B*I}}H50lX=gw!J#~grw z8-;!OvAn-?xYIhMr6tL`f}s<-`kPqau?-O^$7gk3Vc^6nvM-3TlnleO+?>u*~|dp zuGA>KR@=0_(2KCP(pHkz9kCEar$opAiqH@1&U^`hl5obXFXkFBk+ELdqhqM?jS?9? z4A5z-uIfSWutUe+^0u=96Q}*UE)4}|OGFktGkJJL89 zh>pm6&IZb=D+WTCU5mcW*pLs;Q(Y5c_OZZ}&LC{H#UPjq*``Tr7!+KtQN>!44x^4A z4bTZi{|2Ry5*NIq$p##9n~R$q`v4dcld3cz)I?)1| z;Z~C5cT-F$X^}-Y#pH~AaWG2Jw@9H_h_S~AQp9yzDrS((N3L59>*yEy3HJ}JFn zU&i3!(y6y+OyjVe5D2{IdYcf7lTlcoQdkE(4V0wkl1PhPCi>DFK;l0VGaSX#!5MTP zPzV{hViz_G8fC8b4#iW*HRvvhy5n_9SM2BqJl_|3r@7RU+c6=`N&!%Re~vQ4#| z=_aXK5n?|xbq z%IItQI|?U}O-R#;_=>h6zW-0ZJ1ex1zSqRouL@Rf-`Or?Z-~CBh1onl+G6LzlTgPyrUsZ(=&AJeXCS}BIzxxLgbeueP z>=TZ#$eWlZ9vJBkECgTU5b?rp7Ut^*ba0L3f@Y*qIPap>N5d?(diyHxSY*wt1bCc> zUqzV4b`KRhe!O<;z_#?IX(HAIcy+Rh&JflV+|nOHqy!1f-U z>qlx{B|stnN$UkbAr3W5ZY;BR=$c}`V!5w3H3|P?4b^B}zuwtFLMfsHINg9*^d)!D z%Pfy!^sBL zLSStNF{24E=3WCR{GWB}$i5-_qnMY#tPKcKJ_>Gy8)ndP&YRXzf`^?eJR^9cdB!#$ zsOp6$4~R(|U}ZRrmKkPY=W|_NS)Ppep~$G%|(Q$K>4YSTnSF!r-8^3DH4Zo)sx1i2%S&jkMo`>EaR{AiW$a z-Hzx`nnFH<#0}g!h0+@c7*~%n#Ipao14hszMY`uvuXNOG!^$c?0zT4`tcFXg?~~59J5=mcF;Yi(9SFBa7|57H?8`Z znK-LQnM$xp;4`V7=p3DtLEF539h5Y3!VH3@s3$3ihhMSU{SNpC5EA_~(lEKA(9XpD zN2qc%4r=l$eh=wpgKk+WP)JDU3qjKEeV^uj_pd2F)0b2%4+HO*=}UXfMdM(d`5y^~ zq%xJ}V^T=^?r3H!Mye?ixn0~w# zI+KB3YaOB#K0vRa|7j7Xwj1$YOs(rkxr-Q((Y>VD#+5u$C~kLl+RwLX85Xyz=?vfV zRZ9)OKtW->u%qw=5RM=*7EGkN@h2qmDe$>?@8l<= zmBU?H*Yhz2dda^vr#a9^MY=XpU^7cJr(z#cixcs|y?iwwFwQkxpLM$HB$B;It&9^1 zcTA9IHarNb5C=zEpz@*}fh;ihcnOSxe;q9aA)MMGmW8_kPbq-3j0^7WSdT=Y2e_x| zERZw`iS)_C)QiMinJEN)Neu+-i*tHk;2{uTEKO(KmemINZh3@GD?#_;J-66Yvn@CGmK1hp0zCmIi*ugWuYzn-;h(>8?8{i<23|9@<<$l45g?F z!Hq9L8+0X1kWe}AfQz(7-zE$n;D&Di4*}BE%fMy9K$YWgyIjoF?;NfnWEvt0u$hVG zxkk3I4!HlJSE)eu0mhs!%QbD#JvAc?p@Gx|rj3605eb!VtRl9OE8IBXTLsb)f~0!4 z>y)Cxra-^@RT6w=5%&s^tPk$GS*YFbF3ED^D{Fx(82(M`e)qg4=2ordE|?H1UYL}r z1*m!F3soODz0>hpJ926(rxTHw=3P(+XSX?dE-4hhUx`XbvYIqFyWt_E+c{pZs za*lH6OC`54e*(akS%hYAHVYtB zUK)lBphz>R1n0}rM53`$rsI2cf`XiTP2@0lCc9_g%`M~7mkSlJO-2y_fH|S1?^xN2xB4J$y1q$v(xbNfkFQCql_n;@` z)pn6DGl*(Xua}EfI31Fs|l?e_e>iiq7psH0%!o>dGR%U#;-V zZPxv;rGBk>85!24Wr$bCDNJpqtr&csk;w6pLSQUonZ%}X;BbpeF9w-MX3}#{Jmq2Q z%Z5RBUh@>9j`@@bb#StA9>x{yBE9;3!*w(bg%>V?fpr{HK*6g~WULPT#1F5LS|Hz8 z3|S#`6P%0Tj|+tSEDAwby729X@rY69^@c++hd|-wquK9eEc8gu=7nlxGzwV7D0`4A z71K-Pm+Qz3;%BEoXHwTEbocx;Olnb<)j3`z-b zeW?~eV9@<~wVdS^{D6|Imr{gqmWBwS0ErGhsYRxs*Dm0ikssUdQILFx7o@}s#k%C8flBC*@?bOav|Ze%hT`RF>QkmD+ApEPWi zPH9-{BwR4PkFJJgp~&@S+@q_8Wx>Zs!un~%l5z0Uh$X}NX~cZknZ;6>LWgAI8Uvk$ z#s)eIl?`+jIveOLls3>=Xgx^*Ub7KIfo7qG0?k4S1)7Bp3N#B56zCBskcHV)tyy?Y zfj;HO!Sbh(8iGfU0BbqcBu!;iZv`dNuH7bS?-)4!%B{KX7~*IF85VTS%7_tO zfEV189aUOZtBd*@>{q}XDUU#=DBDv(q@uCR4T6DEjuV0PI-Ji{6%sL>@sjH@v-KsDmMAPw8!vMExj-tM2}8D|xuU;HBnP z`Dt%Q6zC{IE3RZQtkIUNidYD~tt3=w6K1{9Y&htCRB5p(Y^5x6jF*s|1}59}@?vPc z_AH~=yp$2M)8jo4aEi<>bli{ubeYWdpHLEE*p8IAsgE z7=x38%Y-44BDz^=IUAsVNjiw5;t`);1I-V*zoIxX7l_l#cnJm{KNPfH?d>%fAw~i# zs&E%&fP?OLWKjz=&Qg*q6S-a*b^EanCVjHy$?ojx#ie>}rBn?~7hm(r*PgxzV0R>Z zKvu6$Vjp~}vAI$V8d+SafW#IO)UP%X)Zc4(pp%QMYc(ump0(J=HQ%T$mHiCV0YJK2 z<{hSLCs_&4RKRF_Lu7oh4>kfAWY2#Ts3#kHWWhSbnO}NCkLy}fRHREwb zTABPt9jnPLwBm`gv{uG(HtH*S0XdPcZ6G;-H>LE_!WN}yQcTpcKc_;#6O{C{e$f-G zAz0)dZ5A4<7m@XJBU^#>QUQCWW?`f3cQVuhZt^t?A!R-UJcEev1==qxmfc|r=@f(R z?<+N7LS>e67?o)X*b2cMTJjGP8BEHSjdsfNYKb&H+Y7($OT6O8s(^MC;3_~!r(yLA zNzJEJz5&a&Wi%p@p1e-YRZEb=Bj6#G>qF>*#x6O+$~@x>a{2N%#k7|3tcF>QQ6Z#f))^z7R_rwq(<<&_b$A0L=0 zSZUTq2C~Ens)wi7{L}^%7o>p>ED-@Meg-OG8BK$wd0cO?(raGi)WQko;`zc_Z6y=* zOz}dwSS^U0@-F6~+NYN#zh!i{pF?7TW}`3c-#9?%!5}(`B@E*N{qjQT5*T~EAtb?F z8@+>TJ><|utmVSS&hE9|&R*+U8&^#E2c2kl7@gs-YrWR~wYH44=#bIv~ckfS-*$xL}aS3yIqFynR={%C@MEvz3aoy zbTkv!bg(~s|FBzo;KZ;yw*jmdB4Lf>-o+Y_O0h>S!iQfI9bpz3^uwS6yde=ZB~V=S zs^7g|WmXS0^EGiZp<2XONt;=ULx0$@a1@^-m-=qW1zLFp$&yP-qD3N%i)mBj>`ZM@(4ikW`7|%8 zG@hwN1(EtPV2){WK_vD}Ehy;cwYedp&yMJ(AgxzaPT>(nCy04H+NyM*srXI?OR5B+ zsU@dm$zaaV)PmEZpt{!)B)a5d0xv#SacJB!384 zazN}v9h#}l7~RlJ6mXYz`tZfh=iAYV z#ztuoYLr+=FO`=Xv^3%DP3ZZRfBVt=Z3tn2K+~0fM?Z5kv4@p^Prvh0J`5{=NKfNp z4h<`RBtFTxIIMg@6r%LF!^$5EFfn09DKdk&9eVc0_ zKp7Gk^b5^i>-w;%t}bcso)yNh0P~rI)0lW0P`cN;>8!Ws|0&(9{(b za}6B5NE!~Jnbn;uSCB?MuG&QJkd&1hDoI$J#$anA$U2BbzQRVk{am{z#@5>4NrF#O z&yl`&kDm(;+`jT$xe7euV-_1Xe%gUNz_X}zJMx6xqa zm$0`%nCr@`=m{YP8uIaWdK_y|y<_qxf_7DM^z-&+ZCDu>FPk;|%+t?tMX3q#zO)5G zP14WYdCG56nWEqMsz{MKtW49B>Lg(@gJ+ObDI+mS#c0hE>=@+$sLat%#9ymXY-DUD zFAgim=oRuy!;V!rJFFZR6-}-VE}MZ-wvWmQ0-b8;tIxyANqPYndAeL#xq}|Wt>ns` z^ivJrUE(*TdLLHq7QbQJM}N-H?+KW8G3%Fxm3zb^od%a+7q8q)k0Lg+{Cppt2hrRT z!u}OX;BgT}n)p#>>8adL@ancp<<$cCw9=Rh#r?zfcBe(r=LEYEeDn}VolR#z00RWbN$33`g+Vibf5TzPEoVXPlrge6!mGTA-p;_avC$~dWfn|^NAL>sPJ~?V9kj0WrA%EaEyB0G@@6WK zquh(b%3J8S9*9TjA-_eWz=%I4KG7uu)Oc1rG{lvI%H#AP6RA|r(Qmi~;Fbo7nkVRS z66`8Bs0-X@+@TC>WN^ptCCQU9N!-|&}R8&>{`0HT9__zzURf}W;9<3@v0 zQv#K*#G3))FG3m{zM3R1mGl0qsDyf^x%OWO_+yAXxL>HY_Ebo7!Hf`Z7_`4i1#(p( z>%Ln4rW27$Q557cX{k^i!GIw&hLr^Z9ba7Agk_GWfSazbEOp&`B#z0Vos3RW|8C=cTAz z5TMJ#{~DH!$`*Sj7J8bV5$pnymnzSQ-%Ch^Ijnpweh;GA#e==;Ev)Xbz)^9aD_gIT;3%6e_IS3b z(y;}Yohw&G{m2(lgR^?&ns}nX8Zg6EI`kw{XH|CS_ta7ut_gLz>rm;^3%UM0M?c4l zYaq<9vMUNTSBrqzqu-MlXj(lnBKzW-xEfhGkZ_v_d{enjzlBd&pNg}YE!+rKl18~Tmv1OJtM>rWHaW&yXi@K-&ei~ zzX#DVV)Ko5Z@Yc{5H=%05PLkoS(Kr?tHa8BL>UoM%$PHlcX|+m!uCO%BP_5GPax=j zB?>kvQby&ktKYbcQ27@6Jyl)@QLyS^B)^qjj8QK0%8U3p7-cU(3snk^Qh7-fY~Vz= z@?QEmzAW-8R=!O_hy9c%!OKqHdy+P37;RBJhmZR4V9q zvuEOy4~XXlh?|BG0pCNdl@LP(D%`EoQtvYH_ane^!@aRvr3vy{|)_~T0qS0 zEyCbNzXq*&r`zQEX({|{(>O>9$pNh6GYDP{x%7{UM!z&ihhuaP819C zwSC0QtNeZah-{R2{O?4?_$m@d;>C~Bi%Gm7I~ksTEPlqc!1F)QPK1ohv;z%Uv=&OJ-wL33sw975I@V>e?pg+wSQQb7nU_N_dntVB$r4}W@Nm2UVu?- zSoud(W=29R_YU?Chn1hCS2;>tH>~`WsKHK9#AdEw_q@#RGpzhH6`Wpzu0y$9;ZRih z$D*o65$>y%p9xgOEttgJA~{s$BUEZ;sRUIKIl&;?hn1fdh410E=7MFKq|!Jm)LiyY zs0#H=4DfUGd`#Z+MWvevegwhxKhfLiSWCxz{CQEXTtAN!P29h%{8Ky+qT{OvhlRx+ z_RM{)3&g7DKLbQRB}D@vhGWuaus$^QVA%#T|FdXTzFXKq@Yl*O(3@FW<2g z1bmP4j#iTIb>7ix@_qDy}1o#Iku)Y-acsI zTmx*%TKMMx?nh9o4`6R-LRC9FxbA>_Qi7-y=Wpm2N?7~tTK+cya(bb?-)_N(O`>HD zQFaanhc?`&1l&nzSXa>w^v~8#k3#X-P@fi1cUojz0F(&0DJU5475@c5VG9HW*UE5| zq3z?Z;uSU)i)5?6+3Hza~Bw>F{<~`9DM* zcSy?=a+qyDC-m7^uQvIMD*uwoDbD;?qS!rBCMc_bn$_aufKb5-iwkiO0l!WKNnAjw z99I4{JyC2r+Lr%@_(Urif*MqQQ`9QL2gPGKuXY2ZTP|SN3YZs>_r&=%I}2gV&-{-Jgm{4``?7%Bnt?8 zF^nbTZv$EIIRWX=LzoQ(Aa*fp2)i0=JcDlkji3R+-9F!0L16J@OJmr3zQyr+Zxk}mP@ zMT;U4tlU}sA-&K>gg>GO#nxXCzokm^$Mid|68Ba9gq|kgVp5}cos~ZofVHP-k@_?G zja!|1KmQ-;d3t%Xxmj1cgD+ZdNDlu6D?`tJNzW4-*q#U__qJceqe^~Mc@;fOz?380 z>?^m^W9~wY3@&5%t<>@wRPsF$Dp<@tHgEr&>b6JkKj4+pGi|cjU^!}Pa!J?(sn_H5 zjO{Wu&oC;+4?tvs9&)R~Mms4!AnYhy4=Ym=YD*~W(*g>nQxu<}-}xef1XRnz%B=VT zB_BoR#Fz6j!fWN2EOU{%dtCfpFOn(kgn)uYY6Te~Dkte_9L_rMQ?8s6538FLHKcNy z9+33_0^tt)REj(7+RwLlVWlTtu{(XItcq-6!^&OsFtJ!$N8DIE-c66#AVGGbzBBZM z_$kfO3WbxX+#^b^Qa)5z80mrC0k7OAUO*p57Uo5GtyS*-PnBJHfL_!2Mlz9vOjg9& zv5$SM!GFOs``Q}6H`}$cMt)JRTQKghpN-4Fq6t&k{N>NHFrIh+T z&-0#p@45H;CjI^LN9KK>^PY3hz29BVdCz;Em%!rWbpm(A0$Y*lfOa~(R-!m);!YDM z@_GE0>~?5JFb35Y6?R}J2Y5|wWoi*ifUTn9Kz?AWN=wHKt5L%j93joNx@7m8t^pb2 zyoIz9Rc*BKUlNHiea4Qv`QLLqg#Cc(BOLlk0I0qlSHDWHGE!i z*RL0$SkUk7CRLrSPjtXOJmv-x5#F`HHl)ap|76ZC@hfjcpvlus*~U`##&io;aud~> z#+S{e5UcR-;WM3`QSs;AjK=Wt(u24;`)#@>ys3vs>v{M}1jtxKDG`+xfl8)sv&)Z~1y(uU=QyDZq{QkSB2&SO>v0bH1 z#$Xl7Zd3;17sWHPJLLfyq)eii&rA2nRlY7KQyns;Jq=s7ju@cDlo;SBcPfG4&EFGp zcx^^W$Ln7O(^O}?Ic>Z)tq_nx0+YF5eRdYfqZ=zE zI=4BhgKr|9pt)*H)3OEDLnTj2Zu26YSMV_EStfDo%~h6Gs5XU8_DKnytWpV`Je+d$ zNj!pjltkN(ltGpX+vZbG(+=BF)C%dU?Pyid{AO=@bdI48XBi#2$5IX%73?@FX+BXq zUS_`Lbb@51inbFeD<8C-L@`hLY$sDspQ=-+-~*)1P(#W7Esj4ZFZ ziN@3R#8id_tuZ!gc|{IA5?`2n6S*H#fZf4VPlS%UapBT|g~M%xV`= zjehg*QTNDq`#$B63Dqu!f*85{NNG}F-oL?XCVNj}YM01f^NLd9Usi~73fd1?FGG00 zUaFQnKcihHC9FkUUf*m;v77hf3RduZoOY#T<=V6#s&3&FJYy-VrsK$~RH1{JFYRho zsLzN?`#i^`{YX_Naj%wLqbiM3EMszztqWWWD+D(+N}kLaoTpE=>l#>PC(Bn6zidC2 zQSpH3S>gMdEmB-ByFy+x&)bFrdgeC!Ndpt5SEGp%tl14RDP$gtGP}X9klW2}tT&Gm zyTRP&gg>o6u;*;!fklvIH`SYX$~KrpN!#pZ*%cFxdB!#vhMaBovw9=&^DtqXZX)Jv zvs+*j`9*GpiWfL<|1>{YG#T{_ZuWDvTgZ%-r*G5Qa#8d)Srx7!=g1*@8&BEaJGaZO za4I2r8*B>c+w2!I)f71GU0a&F4aRBqHoK#~gIGWfsoP+fCvUSm>yI8Xx6v-k-Dba( zUE#u{iQ8ZpQn%S%vht7-WevoYF3l+JmdQf$_2?-L^POM8ILqc{_fS*LHoI3MGqc&R zDUu);Psx3h^BiLwo7`W7_}cjmp(xRiJwPB%<75vO5LSAKNctUnSc>Q3vPYm)YYp>n zm2xuag_7mQvfr|mkfw=;_+9@Em6E`*p)^~SJ<4WM{s4Q78WTI&? zrCJ+POvE*z8>1C>xum-u>`53_Y9kAV3M1t4W2eF&*nA4`RGx|f4(XmI5Ct7QLp{w8 zWY0pYdH`vba0jkpcs=EiDbGruV`aP;IR|*2a!9OWFHrGM`yU<0nm%SBFACp@kuxt* zq~jK2lb0dmLhxzN4;xYo=@j#fHTDYY74y;9tHknbGxi$wur=ELM8z}3*z1y&2gcr@ z9A$a2KdV-_L{f)KqOeFS!T2zh^`%toW_SxSZXI>Ed zNU}11*vFEU%ftRbIb`XuPoPw5V~bazy?bt-R&f0N9kO!Rr>vOf-mrf{bGPqj#FazC zK38*Eu-&n{(%IEJ7h!h#2o$Hk|6&zSi(y|-PBUNFm(oIV3;Q>*o>qV5M zs^u#+V^}HB@7q!`FNH?qJH?)fdEfS?hcZl0_meCxecPXMN|oyagBFs#2Va z-BzO_-gR3YB3@~Q8(2}mnoA)hnB*}|yKZa1w&F1j9x2>#zO^PBcN%qTl`N)oTZ^*D z&~0tO;ybr>Cwr{gu%42Qcp%$pB zZ96J48?|k(;xK|6ubP2V+IE13Il$yu^RbqLHOo0%wa$kkP0k~2JF-$v6>V+QV>W1; zK*WijZ6f8A!`a%YWi-vUlhl-n+01R*u-t~Ha%X6W8jri931v8Ue@u2^73WK~T_xj& zqC-(l1&(9eT_s{Kwn>x&FR|?*vp_v;lcfcAVe62V`p~C9D_xu&u$ap%EPA<$G}!i3 zoxlfd)2KyMzqXeEtbhhKU)!5NM%ZinP<2jT+ZQtK(fIVS#yqdytW^! z#*DkRKaoJHYX`U@-dsD7${=`iNsVgu0us#HOp5g!vNnr&Lz-CYW)TMVoMldhjAd`#ru6C<7NpA_jl<&z_wXM9SegKW`GrJk}cf>F^td$EZPE@Wf312K`pJyO~VSAk4*x>p_CqQ;N1$CrT|9txHN}+T&weB=Bxkp4s5wKQT}wG5-?Qsf#Q7fFdXys)J-c3o zBFeL$K(68~gpn-wQ1Sg*JcS!*98t;f^4>@+W4E)PQjIw5>?Q#@Q=Q#hYJ@k>&xqBt z&e<)*b8b1im3oOm&VDYi$P;I`5e#f_b~_d4e6wGuz?t3b4k|f+o81}d?AI@;XIyM{ z7t}ChZ|kh|f&H_y!Bx6jjc~;3p^CxWPBMFdim!4#DBPLF>>=SK53`3UIzN~_;>A zOWCbh<*68;#nUms_s}y0a_TC3mU_xhWzR{YH;;?g&r=H&Q}zNCzpj6zn2|);i(biz zpzI}TPWEIkQ_dNk>=o(}ZIivKs)3owUZWQCFWH}nILnf~PC4gNvNxy)#w7bQm6R9B z-c&Uo480}UJt2RgoG}^M+fq}qB6~-cSa`#!ze_n~8?wJr%ejQ?J?R-wkiAbm;{dY1 zMaBFPKA@g*_t=L~DSwf_Q%~mvAIUWE>e$CJ&8TziAJj8;9Q%Z7kpRa&6;|N5v45(W zjK;=3qh4UDvCjcv85-+oU_Kz3LN=Poh-vI!tQN7**cWOiPRwmz5(-bnzbU8GFZLC+ z60wVYEtA0AV*jC5WN5K(h&c<3E%F!}3=Au_D7C<%VvDILP^H)a6*(V@4WtqgomdkA zW*5e*V~`4S>JnR=K!Kga2FoTVAh98`i8)4WD501}#FijZVhXV()l|*{V#8v9_eygN z@b(xU0~|Pvhye~9MiMA6ao8w8B-`L-+g+6YhO%cMmO%Mqet3)rhxQ`l6h+*F|lOpjTy7w(Bb@8Of5R zJ7)Ft_H|bKTKalgqP;M_9dl$FM-^3DTArD~fuKVXw*73C#BEod+cF7!F5CmWz?M>{ z%iAG~UEs(dY|XWmaimJ!uEB=qvOdoRK?S=4WeOUqYOc_9$9 zxZkVc&oPNtcc?;k!^PW!$f&Gp^h-=;$;k9BX-O{t=7A`{P^h$-tu`XWV@FL)Qzz>; zt2OM~A!xS2&b8ug7(WR!RH3|4Y;?X)D(XouPi7c)rgyb)D?RUI1o)wQnD6t}?suaU zo|T9Q22p@IrT-UKjIHdKfydeX;7V8T;nPFx&#)}++i#0iV2F%e%w-@!&OjixN0>UG>3C;#Y0HP|&(M^dC&OfZ1L9dU`t}PSd+wf5ZeHb?7WaFUau((+1 zCvJHd<9#FBHMtNuyh9-x5!?#6G~FzPgpSVjs9rmp8;A{jc!VcW9;DrS&^G5;6~ZfR z3qnm3uzL^XTT&U75Bl@d*KCmLXVLUFtWe!x^1yJ6Y+Slt(<| zs~hm;U?{{VNl_4BQSesT_1r#-kf;q$%|nC&BCu7V|M-Rc2#O~F_d=s1WU&Ud=JW{O zNrZCCUIA|9Rqb@ZI0Q4cg)CN&OqX|~J9tS|DXm-c(1*_LIha2EupAgN&l3n&bhK)g z)?VQW$t_k=k4qMqY2_AejVv(r$}J`|vcOy{x0oo4eit}LrYo7zNw1-<*R3y(jyAWp zIDru!pLVpLp7Um?_}|tIv4Kb2N2t1!y0*{-!N}TVz3?@^$b4s+_p@48TQ8|yy=gVF7H|K8N_y4k3mEF_c6hi$`Eq^eyx`!7j!OYbA z55f$f{VqJ!wJ}-!DXb1*AqB*I2=P!Lm)z{7uSW2f*M?DuZkG-vyxly3AmBdC?AdT z_s0%3_5o8rc_YaAnX+yrn~oEUX0kc{I>l6cE9IY~MLF(Iws31SlyW0^`vXu~TdEi* zmwlL`I7kl4LhZ%TU^#(pXB9C@n{64@%2ZnoMa$NVVqn zp4lL0%XUgS!_*S+9L{LjzOxgYZO$@PO*&BL*agW{R&7DmO0uUTdl8ZEB-z`MeTl3h zoBbU*kjRok4szsRBC87Nbfk;OYC>iRX-#GkS{+c*O=%5E38ghD9ZIPcQvFbHEuht8 zHVn|^xwN|)NG0iUILRym}2yF_q!#+N1eV$I z5%~nR*7#|i$Og`31MOd|-F3uEy!}hrJ{?Gvdgy`pL_?yi{LGlDmW~rSgL#?L>b@x{*VL`J`6jSfs z0y~s#Jy2*(R%Vaq4|Jn%^7_{nC~7X!#8A@yh*#G zfmD*WLe;kk9RnzNhtjb!e^-Ucdqj?t`TLH1K;(EKA3E|8krRY`?8qlXP7?B|BcBmD zS;*&(d_m+CAzwQ36_HbgeC^0LL{1a3$ksg1hQIjfGFVIo$w2%O7gHr^a%6EL>&RxX zBSVRtDP##ph7mbSNV6j&h@35Cq>$ERG@%-xdI$YEK�ZH!Qzm&t;Y3EA~7>rC03v zGMmsnBcr1hRa3GI4||~wFjfahmiIp^`Ja{br+%cm097iw1V<5|xO9zh?&Zjy2 z&DNEiLbTT0F@FxW|$_y7O^ literal 135088 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 VN|2s@APVa$lY!lU^DCFP{9o7VyI24K diff --git a/worlds/lingo/data/ids.yaml b/worlds/lingo/data/ids.yaml index 918af7aba923..1fa06d24254f 100644 --- a/worlds/lingo/data/ids.yaml +++ b/worlds/lingo/data/ids.yaml @@ -766,7 +766,6 @@ panels: BOUNCE: 445010 SCRAWL: 445011 PLUNGE: 445012 - LEAP: 445013 Rhyme Room (Circle): BIRD: 445014 LETTER: 445015 @@ -790,6 +789,7 @@ panels: GEM: 445031 INNOVATIVE (Top): 445032 INNOVATIVE (Bottom): 445033 + LEAP: 445013 Room Room: DOOR (1): 445034 DOOR (2): 445035 diff --git a/worlds/lingo/datatypes.py b/worlds/lingo/datatypes.py index e466558f87ff..36141daa4106 100644 --- a/worlds/lingo/datatypes.py +++ b/worlds/lingo/datatypes.py @@ -63,6 +63,7 @@ class Panel(NamedTuple): exclude_reduce: bool achievement: bool non_counting: bool + location_name: Optional[str] class Painting(NamedTuple): diff --git a/worlds/lingo/locations.py b/worlds/lingo/locations.py index 5ffedee36799..c527e522fb06 100644 --- a/worlds/lingo/locations.py +++ b/worlds/lingo/locations.py @@ -39,7 +39,7 @@ def load_location_data(): for room_name, panels in PANELS_BY_ROOM.items(): for panel_name, panel in panels.items(): - location_name = f"{room_name} - {panel_name}" + location_name = f"{room_name} - {panel_name}" if panel.location_name is None else panel.location_name classification = LocationClassification.insanity if panel.check: diff --git a/worlds/lingo/utils/pickle_static_data.py b/worlds/lingo/utils/pickle_static_data.py index 10ec69be3537..e40c21ce3e6a 100644 --- a/worlds/lingo/utils/pickle_static_data.py +++ b/worlds/lingo/utils/pickle_static_data.py @@ -150,8 +150,6 @@ def process_entrance(source_room, doors, room_obj): def process_panel(room_name, panel_name, panel_data): global PANELS_BY_ROOM - full_name = f"{room_name} - {panel_name}" - # required_room can either be a single room or a list of rooms. if "required_room" in panel_data: if isinstance(panel_data["required_room"], list): @@ -229,8 +227,13 @@ def process_panel(room_name, panel_name, panel_data): else: non_counting = False + if "location_name" in panel_data: + location_name = panel_data["location_name"] + else: + location_name = None + panel_obj = Panel(required_rooms, required_doors, required_panels, colors, check, event, exclude_reduce, - achievement, non_counting) + achievement, non_counting, location_name) PANELS_BY_ROOM[room_name][panel_name] = panel_obj diff --git a/worlds/lingo/utils/validate_config.rb b/worlds/lingo/utils/validate_config.rb index 831fee2ad312..498980bb719a 100644 --- a/worlds/lingo/utils/validate_config.rb +++ b/worlds/lingo/utils/validate_config.rb @@ -39,11 +39,12 @@ mentioned_panels = Set[] mentioned_sunwarp_entrances = Set[] mentioned_sunwarp_exits = Set[] +mentioned_paintings = Set[] door_groups = {} directives = Set["entrances", "panels", "doors", "paintings", "sunwarps", "progression"] -panel_directives = Set["id", "required_room", "required_door", "required_panel", "colors", "check", "exclude_reduce", "tag", "link", "subtag", "achievement", "copy_to_sign", "non_counting", "hunt"] +panel_directives = Set["id", "required_room", "required_door", "required_panel", "colors", "check", "exclude_reduce", "tag", "link", "subtag", "achievement", "copy_to_sign", "non_counting", "hunt", "location_name"] door_directives = Set["id", "painting_id", "panels", "item_name", "item_group", "location_name", "skip_location", "skip_item", "door_group", "include_reduce", "event", "warp_id"] painting_directives = Set["id", "enter_only", "exit_only", "orientation", "required_door", "required", "required_when_no_doors", "move", "req_blocked", "req_blocked_when_no_doors"] @@ -257,6 +258,12 @@ unless paintings.include? painting["id"] then puts "#{room_name} :::: Invalid Painting ID #{painting["id"]}" end + + if mentioned_paintings.include?(painting["id"]) then + puts "Painting #{painting["id"]} is mentioned more than once" + else + mentioned_paintings.add(painting["id"]) + end else puts "#{room_name} :::: Painting is missing an ID" end From 92392c0e65854fd01d9fd7253aa1ce8ab8ae96a3 Mon Sep 17 00:00:00 2001 From: Justus Lind Date: Thu, 23 May 2024 10:11:27 +1000 Subject: [PATCH 07/64] Update Song List to Muse Dash 4.3.0 (#3216) --- worlds/musedash/MuseDashData.txt | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/worlds/musedash/MuseDashData.txt b/worlds/musedash/MuseDashData.txt index 0a8beba37b44..b0f3b80c997a 100644 --- a/worlds/musedash/MuseDashData.txt +++ b/worlds/musedash/MuseDashData.txt @@ -538,10 +538,16 @@ Reality Show|71-2|Valentine Stage|False|5|7|10| SIG feat.Tobokegao|71-3|Valentine Stage|True|3|6|8| Rose Love|71-4|Valentine Stage|True|2|4|7| Euphoria|71-5|Valentine Stage|True|1|3|6| -P E R O P E R O Brother Dance|72-0|Legends of Muse Warriors|False|0|?|0| +P E R O P E R O Brother Dance|72-0|Legends of Muse Warriors|True|0|?|0| PA PPA PANIC|72-1|Legends of Muse Warriors|False|4|8|10| -How To Make Music Game Song!|72-2|Legends of Muse Warriors|False|6|8|10|11 -Re Re|72-3|Legends of Muse Warriors|False|7|9|11|12 -Marmalade Twins|72-4|Legends of Muse Warriors|False|5|8|10| -DOMINATOR|72-5|Legends of Muse Warriors|False|7|9|11| -Teshikani TESHiKANi|72-6|Legends of Muse Warriors|False|5|7|9| +How To Make Music Game Song!|72-2|Legends of Muse Warriors|True|6|8|10|11 +Re Re|72-3|Legends of Muse Warriors|True|7|9|11|12 +Marmalade Twins|72-4|Legends of Muse Warriors|True|5|8|10| +DOMINATOR|72-5|Legends of Muse Warriors|True|7|9|11| +Teshikani TESHiKANi|72-6|Legends of Muse Warriors|True|5|7|9| +Urban Magic|73-0|Happy Otaku Pack Vol.19|True|3|5|7| +Maid's Prank|73-1|Happy Otaku Pack Vol.19|True|5|7|10| +Dance Dance Good Night Dance|73-2|Happy Otaku Pack Vol.19|True|2|4|7| +Ops Limone|73-3|Happy Otaku Pack Vol.19|True|5|8|11| +NOVA|73-4|Happy Otaku Pack Vol.19|True|6|8|10| +Heaven's Gradius|73-5|Happy Otaku Pack Vol.19|True|6|8|10| From a43e29478646ff608c46238ce5c20dfe4d30e687 Mon Sep 17 00:00:00 2001 From: Scipio Wright Date: Wed, 22 May 2024 20:12:59 -0400 Subject: [PATCH 08/64] TUNIC: Add option presets (#3377) * Add option presets * why the hell is there an s here * entrance rando yes --- worlds/tunic/__init__.py | 3 ++- worlds/tunic/options.py | 23 ++++++++++++++++++++++- 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/worlds/tunic/__init__.py b/worlds/tunic/__init__.py index 8e8957144de6..cff8c39c9fea 100644 --- a/worlds/tunic/__init__.py +++ b/worlds/tunic/__init__.py @@ -8,7 +8,7 @@ from .regions import tunic_regions from .er_scripts import create_er_regions from .er_data import portal_mapping -from .options import TunicOptions, EntranceRando, tunic_option_groups +from .options import TunicOptions, EntranceRando, tunic_option_groups, tunic_option_presets from worlds.AutoWorld import WebWorld, World from worlds.generic import PlandoConnection from decimal import Decimal, ROUND_HALF_UP @@ -28,6 +28,7 @@ class TunicWeb(WebWorld): theme = "grassFlowers" game = "TUNIC" option_groups = tunic_option_groups + options_presets = tunic_option_presets class TunicItem(Item): diff --git a/worlds/tunic/options.py b/worlds/tunic/options.py index 1f12b5053dc9..a45ee71b0557 100644 --- a/worlds/tunic/options.py +++ b/worlds/tunic/options.py @@ -1,5 +1,5 @@ from dataclasses import dataclass - +from typing import Dict, Any from Options import (DefaultOnToggle, Toggle, StartInventoryPool, Choice, Range, TextChoice, PerGameCommonOptions, OptionGroup) @@ -199,3 +199,24 @@ class TunicOptions(PerGameCommonOptions): Maskless, ]) ] + +tunic_option_presets: Dict[str, Dict[str, Any]] = { + "Sync": { + "ability_shuffling": True, + }, + "Async": { + "progression_balancing": 0, + "ability_shuffling": True, + "shuffle_ladders": True, + "laurels_location": "10_fairies", + }, + "Glace Mode": { + "accessibility": "minimal", + "ability_shuffling": True, + "entrance_rando": "yes", + "fool_traps": "onslaught", + "logic_rules": "unrestricted", + "maskless": True, + "lanternless": True, + }, +} From 56d01f391331357485bf40ab59157f0c40ad143e Mon Sep 17 00:00:00 2001 From: LiquidCat64 <74896918+LiquidCat64@users.noreply.github.com> Date: Wed, 22 May 2024 18:16:13 -0600 Subject: [PATCH 09/64] CV64: Add option groups (#3360) * Add the option groups. * Get rid of all mid-sentence line breaks. --- worlds/cv64/__init__.py | 4 +- worlds/cv64/options.py | 302 ++++++++++++++++++++++++++-------------- 2 files changed, 203 insertions(+), 103 deletions(-) diff --git a/worlds/cv64/__init__.py b/worlds/cv64/__init__.py index 84bf03ff27aa..2f483cd4d919 100644 --- a/worlds/cv64/__init__.py +++ b/worlds/cv64/__init__.py @@ -8,7 +8,7 @@ from .items import CV64Item, filler_item_names, get_item_info, get_item_names_to_ids, get_item_counts from .locations import CV64Location, get_location_info, verify_locations, get_location_names_to_ids, base_id from .entrances import verify_entrances, get_warp_entrances -from .options import CV64Options, CharacterStages, DraculasCondition, SubWeaponShuffle +from .options import CV64Options, cv64_option_groups, CharacterStages, DraculasCondition, SubWeaponShuffle from .stages import get_locations_from_stage, get_normal_stage_exits, vanilla_stage_order, \ shuffle_stages, generate_warps, get_region_names from .regions import get_region_info @@ -45,6 +45,8 @@ class CV64Web(WebWorld): ["Liquid Cat"] )] + option_groups = cv64_option_groups + class CV64World(World): """ diff --git a/worlds/cv64/options.py b/worlds/cv64/options.py index da2b9f949662..93b417ad26fd 100644 --- a/worlds/cv64/options.py +++ b/worlds/cv64/options.py @@ -1,10 +1,11 @@ from dataclasses import dataclass -from Options import Choice, DefaultOnToggle, Range, Toggle, PerGameCommonOptions, StartInventoryPool +from Options import OptionGroup, Choice, DefaultOnToggle, Range, Toggle, PerGameCommonOptions, StartInventoryPool class CharacterStages(Choice): - """Whether to include Reinhardt-only stages, Carrie-only stages, or both with or without branching paths at the end - of Villa and Castle Center.""" + """ + Whether to include Reinhardt-only stages, Carrie-only stages, or both with or without branching paths at the end of Villa and Castle Center. + """ display_name = "Character Stages" option_both = 0 option_branchless_both = 1 @@ -14,14 +15,18 @@ class CharacterStages(Choice): class StageShuffle(Toggle): - """Shuffles which stages appear in which stage slots. Villa and Castle Center will never appear in any character - stage slots if Character Stages is set to Both; they can only be somewhere on the main path. - Castle Keep will always be at the end of the line.""" + """ + Shuffles which stages appear in which stage slots. + Villa and Castle Center will never appear in any character stage slots if Character Stages is set to Both; they can only be somewhere on the main path. + Castle Keep will always be at the end of the line. + """ display_name = "Stage Shuffle" class StartingStage(Choice): - """Which stage to start at if Stage Shuffle is turned on.""" + """ + Which stage to start at if Stage Shuffle is turned on. + """ display_name = "Starting Stage" option_forest_of_silence = 0 option_castle_wall = 1 @@ -39,8 +44,9 @@ class StartingStage(Choice): class WarpOrder(Choice): - """Arranges the warps in the warp menu in whichever stage order chosen, - thereby changing the order they are unlocked in.""" + """ + Arranges the warps in the warp menu in whichever stage order chosen, thereby changing the order they are unlocked in. + """ display_name = "Warp Order" option_seed_stage_order = 0 option_vanilla_stage_order = 1 @@ -49,7 +55,9 @@ class WarpOrder(Choice): class SubWeaponShuffle(Choice): - """Shuffles all sub-weapons in the game within each other in their own pool or in the main item pool.""" + """ + Shuffles all sub-weapons in the game within each other in their own pool or in the main item pool. + """ display_name = "Sub-weapon Shuffle" option_off = 0 option_own_pool = 1 @@ -58,8 +66,10 @@ class SubWeaponShuffle(Choice): class SpareKeys(Choice): - """Puts an additional copy of every non-Special key item in the pool for every key item that there is. - Chance gives each key item a 50% chance of having a duplicate instead of guaranteeing one for all of them.""" + """ + Puts an additional copy of every non-Special key item in the pool for every key item that there is. + Chance gives each key item a 50% chance of having a duplicate instead of guaranteeing one for all of them. + """ display_name = "Spare Keys" option_off = 0 option_on = 1 @@ -68,14 +78,17 @@ class SpareKeys(Choice): class HardItemPool(Toggle): - """Replaces some items in the item pool with less valuable ones, to make the item pool sort of resemble Hard Mode - in the PAL version.""" + """ + Replaces some items in the item pool with less valuable ones, to make the item pool sort of resemble Hard Mode in the PAL version. + """ display_name = "Hard Item Pool" class Special1sPerWarp(Range): - """Sets how many Special1 jewels are needed per warp menu option unlock. - This will decrease until the number x 7 is less than or equal to the Total Specail1s if it isn't already.""" + """ + Sets how many Special1 jewels are needed per warp menu option unlock. + This will decrease until the number x 7 is less than or equal to the Total Specail1s if it isn't already. + """ range_start = 1 range_end = 10 default = 1 @@ -83,7 +96,9 @@ class Special1sPerWarp(Range): class TotalSpecial1s(Range): - """Sets how many Speical1 jewels are in the pool in total.""" + """ + Sets how many Speical1 jewels are in the pool in total. + """ range_start = 7 range_end = 70 default = 7 @@ -91,11 +106,13 @@ class TotalSpecial1s(Range): class DraculasCondition(Choice): - """Sets the requirement for unlocking and opening the door to Dracula's chamber. + """ + Sets the requirement for unlocking and opening the door to Dracula's chamber. None: No requirement. Door is unlocked from the start. Crystal: Activate the big crystal in Castle Center's basement. Neither boss afterwards has to be defeated. Bosses: Kill a specified number of bosses with health bars and claim their Trophies. - Specials: Find a specified number of Special2 jewels shuffled in the main item pool.""" + Specials: Find a specified number of Special2 jewels shuffled in the main item pool. + """ display_name = "Dracula's Condition" option_none = 0 option_crystal = 1 @@ -105,7 +122,9 @@ class DraculasCondition(Choice): class PercentSpecial2sRequired(Range): - """Percentage of Special2s required to enter Dracula's chamber when Dracula's Condition is Special2s.""" + """ + Percentage of Special2s required to enter Dracula's chamber when Dracula's Condition is Special2s. + """ range_start = 1 range_end = 100 default = 80 @@ -113,7 +132,9 @@ class PercentSpecial2sRequired(Range): class TotalSpecial2s(Range): - """How many Speical2 jewels are in the pool in total when Dracula's Condition is Special2s.""" + """ + How many Speical2 jewels are in the pool in total when Dracula's Condition is Special2s. + """ range_start = 1 range_end = 70 default = 25 @@ -121,58 +142,70 @@ class TotalSpecial2s(Range): class BossesRequired(Range): - """How many bosses need to be defeated to enter Dracula's chamber when Dracula's Condition is set to Bosses. - This will automatically adjust if there are fewer available bosses than the chosen number.""" + """ + How many bosses need to be defeated to enter Dracula's chamber when Dracula's Condition is set to Bosses. + This will automatically adjust if there are fewer available bosses than the chosen number. + """ range_start = 1 range_end = 16 - default = 14 + default = 12 display_name = "Bosses Required" class CarrieLogic(Toggle): - """Adds the 2 checks inside Underground Waterway's crawlspace to the pool. + """ + Adds the 2 checks inside Underground Waterway's crawlspace to the pool. If you (and everyone else if racing the same seed) are planning to only ever play Reinhardt, don't enable this. - Can be combined with Hard Logic to include Carrie-only tricks.""" + Can be combined with Hard Logic to include Carrie-only tricks. + """ display_name = "Carrie Logic" class HardLogic(Toggle): - """Properly considers sequence break tricks in logic (i.e. maze skip). Can be combined with Carrie Logic to include - Carrie-only tricks. - See the Game Page for a full list of tricks and glitches that may be logically required.""" + """ + Properly considers sequence break tricks in logic (i.e. maze skip). Can be combined with Carrie Logic to include Carrie-only tricks. + See the Game Page for a full list of tricks and glitches that may be logically required. + """ display_name = "Hard Logic" class MultiHitBreakables(Toggle): - """Adds the items that drop from the objects that break in three hits to the pool. There are 18 of these throughout - the game, adding up to 79 or 80 checks (depending on sub-weapons - being shuffled anywhere or not) in total with all stages. - The game will be modified to - remember exactly which of their items you've picked up instead of simply whether they were broken or not.""" + """ + Adds the items that drop from the objects that break in three hits to the pool. + There are 18 of these throughout the game, adding up to 79 or 80 checks (depending on sub-weapons being shuffled anywhere or not) in total with all stages. + The game will be modified to remember exactly which of their items you've picked up instead of simply whether they were broken or not. + """ display_name = "Multi-hit Breakables" class EmptyBreakables(Toggle): - """Adds 9 check locations in the form of breakables that normally have nothing (all empty Forest coffins, etc.) - and some additional Red Jewels and/or moneybags into the item pool to compensate.""" + """ + Adds 9 check locations in the form of breakables that normally have nothing (all empty Forest coffins, etc.) and some additional Red Jewels and/or moneybags into the item pool to compensate. + """ display_name = "Empty Breakables" class LizardLockerItems(Toggle): - """Adds the 6 items inside Castle Center 2F's Lizard-man generators to the pool. - Picking up all of these can be a very tedious luck-based process, so they are off by default.""" + """ + Adds the 6 items inside Castle Center 2F's Lizard-man generators to the pool. + Picking up all of these can be a very tedious luck-based process, so they are off by default. + """ display_name = "Lizard Locker Items" class Shopsanity(Toggle): - """Adds 7 one-time purchases from Renon's shop into the location pool. After buying an item from a slot, it will - revert to whatever it is in the vanilla game.""" + """ + Adds 7 one-time purchases from Renon's shop into the location pool. + After buying an item from a slot, it will revert to whatever it is in the vanilla game. + """ display_name = "Shopsanity" class ShopPrices(Choice): - """Randomizes the amount of gold each item costs in Renon's shop. - Use the below options to control how much or little an item can cost.""" + """ + Randomizes the amount of gold each item costs in Renon's shop. + Use the Minimum and Maximum Gold Price options to control how much or how little an item can cost. + """ display_name = "Shop Prices" option_vanilla = 0 option_randomized = 1 @@ -180,7 +213,9 @@ class ShopPrices(Choice): class MinimumGoldPrice(Range): - """The lowest amount of gold an item can cost in Renon's shop, divided by 100.""" + """ + The lowest amount of gold an item can cost in Renon's shop, divided by 100. + """ display_name = "Minimum Gold Price" range_start = 1 range_end = 50 @@ -188,7 +223,9 @@ class MinimumGoldPrice(Range): class MaximumGoldPrice(Range): - """The highest amount of gold an item can cost in Renon's shop, divided by 100.""" + """ + The highest amount of gold an item can cost in Renon's shop, divided by 100. + """ display_name = "Maximum Gold Price" range_start = 1 range_end = 50 @@ -196,8 +233,9 @@ class MaximumGoldPrice(Range): class PostBehemothBoss(Choice): - """Sets which boss is fought in the vampire triplets' room in Castle Center by which characters after defeating - Behemoth.""" + """ + Sets which boss is fought in the vampire triplets' room in Castle Center by which characters after defeating Behemoth. + """ display_name = "Post-Behemoth Boss" option_vanilla = 0 option_inverted = 1 @@ -207,7 +245,9 @@ class PostBehemothBoss(Choice): class RoomOfClocksBoss(Choice): - """Sets which boss is fought at Room of Clocks by which characters.""" + """ + Sets which boss is fought at Room of Clocks by which characters. + """ display_name = "Room of Clocks Boss" option_vanilla = 0 option_inverted = 1 @@ -217,7 +257,9 @@ class RoomOfClocksBoss(Choice): class RenonFightCondition(Choice): - """Sets the condition on which the Renon fight will trigger.""" + """ + Sets the condition on which the Renon fight will trigger. + """ display_name = "Renon Fight Condition" option_never = 0 option_spend_30k = 1 @@ -226,7 +268,9 @@ class RenonFightCondition(Choice): class VincentFightCondition(Choice): - """Sets the condition on which the vampire Vincent fight will trigger.""" + """ + Sets the condition on which the vampire Vincent fight will trigger. + """ display_name = "Vincent Fight Condition" option_never = 0 option_wait_16_days = 1 @@ -235,7 +279,9 @@ class VincentFightCondition(Choice): class BadEndingCondition(Choice): - """Sets the condition on which the currently-controlled character's Bad Ending will trigger.""" + """ + Sets the condition on which the currently-controlled character's Bad Ending will trigger. + """ display_name = "Bad Ending Condition" option_never = 0 option_kill_vincent = 1 @@ -244,24 +290,32 @@ class BadEndingCondition(Choice): class IncreaseItemLimit(DefaultOnToggle): - """Increases the holding limit of usable items from 10 to 99 of each item.""" + """ + Increases the holding limit of usable items from 10 to 99 of each item. + """ display_name = "Increase Item Limit" class NerfHealingItems(Toggle): - """Decreases the amount of health healed by Roast Chickens to 25%, Roast Beefs to 50%, and Healing Kits to 80%.""" + """ + Decreases the amount of health healed by Roast Chickens to 25%, Roast Beefs to 50%, and Healing Kits to 80%. + """ display_name = "Nerf Healing Items" class LoadingZoneHeals(DefaultOnToggle): - """Whether end-of-level loading zones restore health and cure status aliments or not. - Recommended off for those looking for more of a survival horror experience!""" + """ + Whether end-of-level loading zones restore health and cure status aliments or not. + Recommended off for those looking for more of a survival horror experience! + """ display_name = "Loading Zone Heals" class InvisibleItems(Choice): - """Sets which items are visible in their locations and which are invisible until picked up. - 'Chance' gives each item a 50/50 chance of being visible or invisible.""" + """ + Sets which items are visible in their locations and which are invisible until picked up. + 'Chance' gives each item a 50/50 chance of being visible or invisible. + """ display_name = "Invisible Items" option_vanilla = 0 option_reveal_all = 1 @@ -271,21 +325,25 @@ class InvisibleItems(Choice): class DropPreviousSubWeapon(Toggle): - """When receiving a sub-weapon, the one you had before will drop behind you, so it can be taken back if desired.""" + """ + When receiving a sub-weapon, the one you had before will drop behind you, so it can be taken back if desired. + """ display_name = "Drop Previous Sub-weapon" class PermanentPowerUps(Toggle): - """Replaces PowerUps with PermaUps, which upgrade your B weapon level permanently and will stay even after - dying and/or continuing. - To compensate, only two will be in the pool overall, and they will not drop from any enemy or projectile.""" + """ + Replaces PowerUps with PermaUps, which upgrade your B weapon level permanently and will stay even after dying and/or continuing. + To compensate, only two will be in the pool overall, and they will not drop from any enemy or projectile. + """ display_name = "Permanent PowerUps" class IceTrapPercentage(Range): - """Replaces a percentage of junk items with Ice Traps. - These will be visibly disguised as other items, and receiving one will freeze you - as if you were hit by Camilla's ice cloud attack.""" + """ + Replaces a percentage of junk items with Ice Traps. + These will be visibly disguised as other items, and receiving one will freeze you as if you were hit by Camilla's ice cloud attack. + """ display_name = "Ice Trap Percentage" range_start = 0 range_end = 100 @@ -293,7 +351,9 @@ class IceTrapPercentage(Range): class IceTrapAppearance(Choice): - """What items Ice Traps can possibly be disguised as.""" + """ + What items Ice Traps can possibly be disguised as. + """ display_name = "Ice Trap Appearance" option_major_only = 0 option_junk_only = 1 @@ -302,31 +362,34 @@ class IceTrapAppearance(Choice): class DisableTimeRestrictions(Toggle): - """Disables the restriction on every event and door that requires the current time - to be within a specific range, so they can be triggered at any time. + """ + Disables the restriction on every event and door that requires the current time to be within a specific range, so they can be triggered at any time. This includes all sun/moon doors and, in the Villa, the meeting with Rosa and the fountain pillar. - The Villa coffin is not affected by this.""" + The Villa coffin is not affected by this. + """ display_name = "Disable Time Requirements" class SkipGondolas(Toggle): - """Makes jumping on and activating a gondola in Tunnel instantly teleport you - to the other station, thereby skipping the entire three-minute ride. - The item normally at the gondola transfer point is moved to instead be - near the red gondola at its station.""" + """ + Makes jumping on and activating a gondola in Tunnel instantly teleport you to the other station, thereby skipping the entire three-minute ride. + The item normally at the gondola transfer point is moved to instead be near the red gondola at its station. + """ display_name = "Skip Gondolas" class SkipWaterwayBlocks(Toggle): - """Opens the door to the third switch in Underground Waterway from the start so that the jumping across floating - brick platforms won't have to be done. Shopping at the Contract on the other side of them may still be logically - required if Shopsanity is on.""" + """ + Opens the door to the third switch in Underground Waterway from the start so that the jumping across floating brick platforms won't have to be done. + Shopping at the Contract on the other side of them may still be logically required if Shopsanity is on. + """ display_name = "Skip Waterway Blocks" class Countdown(Choice): - """Displays, near the HUD clock and below the health bar, the number of unobtained progression-marked items - or the total check locations remaining in the stage you are currently in.""" + """ + Displays, near the HUD clock and below the health bar, the number of unobtained progression-marked items or the total check locations remaining in the stage you are currently in. + """ display_name = "Countdown" option_none = 0 option_majors = 1 @@ -335,19 +398,21 @@ class Countdown(Choice): class BigToss(Toggle): - """Makes every non-immobilizing damage source launch you as if you got hit by Behemoth's charge. + """ + Makes every non-immobilizing damage source launch you as if you got hit by Behemoth's charge. Press A while tossed to cancel the launch momentum and avoid being thrown off ledges. Hold Z to have all incoming damage be treated as it normally would. - Any tricks that might be possible with it are NOT considered in logic by any options.""" + Any tricks that might be possible with it are not in logic. + """ display_name = "Big Toss" class PantherDash(Choice): - """Hold C-right at any time to sprint way faster. Any tricks that might be - possible with it are NOT considered in logic by any options and any boss - fights with boss health meters, if started, are expected to be finished - before leaving their arenas if Dracula's Condition is bosses. Jumpless will - prevent jumping while moving at the increased speed to ensure logic cannot be broken with it.""" + """ + Hold C-right at any time to sprint way faster. + Any tricks that are possible with it are not in logic and any boss fights with boss health meters, if started, are expected to be finished before leaving their arenas if Dracula's Condition is bosses. + Jumpless will prevent jumping while moving at the increased speed to make logic harder to break with it. + """ display_name = "Panther Dash" option_off = 0 option_on = 1 @@ -356,19 +421,25 @@ class PantherDash(Choice): class IncreaseShimmySpeed(Toggle): - """Increases the speed at which characters shimmy left and right while hanging on ledges.""" + """ + Increases the speed at which characters shimmy left and right while hanging on ledges. + """ display_name = "Increase Shimmy Speed" class FallGuard(Toggle): - """Removes fall damage from landing too hard. Note that falling for too long will still result in instant death.""" + """ + Removes fall damage from landing too hard. Note that falling for too long will still result in instant death. + """ display_name = "Fall Guard" class BackgroundMusic(Choice): - """Randomizes or disables the music heard throughout the game. + """ + Randomizes or disables the music heard throughout the game. Randomized music is split into two pools: songs that loop and songs that don't. - The "lead-in" versions of some songs will be paired accordingly.""" + The "lead-in" versions of some songs will be paired accordingly. + """ display_name = "Background Music" option_normal = 0 option_disabled = 1 @@ -377,8 +448,10 @@ class BackgroundMusic(Choice): class MapLighting(Choice): - """Randomizes the lighting color RGB values on every map during every time of day to be literally anything. - The colors and/or shading of the following things are affected: fog, maps, player, enemies, and some objects.""" + """ + Randomizes the lighting color RGB values on every map during every time of day to be literally anything. + The colors and/or shading of the following things are affected: fog, maps, player, enemies, and some objects. + """ display_name = "Map Lighting" option_normal = 0 option_randomized = 1 @@ -386,12 +459,16 @@ class MapLighting(Choice): class CinematicExperience(Toggle): - """Enables an unused film reel effect on every cutscene in the game. Purely cosmetic.""" + """ + Enables an unused film reel effect on every cutscene in the game. Purely cosmetic. + """ display_name = "Cinematic Experience" class WindowColorR(Range): - """The red value for the background color of the text windows during gameplay.""" + """ + The red value for the background color of the text windows during gameplay. + """ display_name = "Window Color R" range_start = 0 range_end = 15 @@ -399,7 +476,9 @@ class WindowColorR(Range): class WindowColorG(Range): - """The green value for the background color of the text windows during gameplay.""" + """ + The green value for the background color of the text windows during gameplay. + """ display_name = "Window Color G" range_start = 0 range_end = 15 @@ -407,7 +486,9 @@ class WindowColorG(Range): class WindowColorB(Range): - """The blue value for the background color of the text windows during gameplay.""" + """ + The blue value for the background color of the text windows during gameplay. + """ display_name = "Window Color B" range_start = 0 range_end = 15 @@ -415,7 +496,9 @@ class WindowColorB(Range): class WindowColorA(Range): - """The alpha value for the background color of the text windows during gameplay.""" + """ + The alpha value for the background color of the text windows during gameplay. + """ display_name = "Window Color A" range_start = 0 range_end = 15 @@ -423,9 +506,10 @@ class WindowColorA(Range): class DeathLink(Choice): - """When you die, everyone dies. Of course the reverse is true too. - Explosive: Makes received DeathLinks kill you via the Magical Nitro explosion - instead of the normal death animation.""" + """ + When you die, everyone dies. Of course the reverse is true too. + Explosive: Makes received DeathLinks kill you via the Magical Nitro explosion instead of the normal death animation. + """ display_name = "DeathLink" option_off = 0 alias_no = 0 @@ -437,6 +521,7 @@ class DeathLink(Choice): @dataclass class CV64Options(PerGameCommonOptions): + start_inventory_from_pool: StartInventoryPool character_stages: CharacterStages stage_shuffle: StageShuffle starting_stage: StartingStage @@ -479,13 +564,26 @@ class CV64Options(PerGameCommonOptions): big_toss: BigToss panther_dash: PantherDash increase_shimmy_speed: IncreaseShimmySpeed - background_music: BackgroundMusic - map_lighting: MapLighting - fall_guard: FallGuard - cinematic_experience: CinematicExperience window_color_r: WindowColorR window_color_g: WindowColorG window_color_b: WindowColorB window_color_a: WindowColorA + background_music: BackgroundMusic + map_lighting: MapLighting + fall_guard: FallGuard + cinematic_experience: CinematicExperience death_link: DeathLink - start_inventory_from_pool: StartInventoryPool + + +cv64_option_groups = [ + OptionGroup("gameplay tweaks", [ + HardItemPool, ShopPrices, MinimumGoldPrice, MaximumGoldPrice, PostBehemothBoss, RoomOfClocksBoss, + RenonFightCondition, VincentFightCondition, BadEndingCondition, IncreaseItemLimit, NerfHealingItems, + LoadingZoneHeals, InvisibleItems, DropPreviousSubWeapon, PermanentPowerUps, IceTrapPercentage, + IceTrapAppearance, DisableTimeRestrictions, SkipGondolas, SkipWaterwayBlocks, Countdown, BigToss, PantherDash, + IncreaseShimmySpeed, FallGuard, DeathLink + ]), + OptionGroup("cosmetics", [ + WindowColorR, WindowColorG, WindowColorB, WindowColorA, BackgroundMusic, MapLighting, CinematicExperience + ]) +] From 89d0dae299d35e8fd8bd0dae4dc10a84f32ccd34 Mon Sep 17 00:00:00 2001 From: agilbert1412 Date: Thu, 23 May 2024 03:22:28 +0300 Subject: [PATCH 10/64] Stardew valley: Create Option Groups (#3376) * - Fix link in Stardew Setup Guide * - Create option groups for Stardew Valley * - Cleaned up the imports * - Fixed double quotes and trailing comma * - Improve order in the multipliers category --- worlds/stardew_valley/__init__.py | 2 + worlds/stardew_valley/option_groups.py | 65 ++++++++++++++++++++++++++ worlds/stardew_valley/options.py | 14 +++--- 3 files changed, 74 insertions(+), 7 deletions(-) create mode 100644 worlds/stardew_valley/option_groups.py diff --git a/worlds/stardew_valley/__init__.py b/worlds/stardew_valley/__init__.py index 6a82a2a26dd8..dafb1c64730f 100644 --- a/worlds/stardew_valley/__init__.py +++ b/worlds/stardew_valley/__init__.py @@ -13,6 +13,7 @@ from .logic.bundle_logic import BundleLogic from .logic.logic import StardewLogic from .logic.time_logic import MAX_MONTHS +from .option_groups import sv_option_groups from .options import StardewValleyOptions, SeasonRandomization, Goal, BundleRandomization, BundlePrice, NumberOfLuckBuffs, NumberOfMovementBuffs, \ BackpackProgression, BuildingProgression, ExcludeGingerIsland, TrapItems, EntranceRandomization from .presets import sv_options_presets @@ -39,6 +40,7 @@ class StardewWebWorld(WebWorld): theme = "dirt" bug_report_page = "https://github.com/agilbert1412/StardewArchipelago/issues/new?labels=bug&title=%5BBug%5D%3A+Brief+Description+of+bug+here" options_presets = sv_options_presets + option_groups = sv_option_groups tutorials = [ Tutorial( diff --git a/worlds/stardew_valley/option_groups.py b/worlds/stardew_valley/option_groups.py new file mode 100644 index 000000000000..50709c10fd49 --- /dev/null +++ b/worlds/stardew_valley/option_groups.py @@ -0,0 +1,65 @@ +from Options import OptionGroup, DeathLink, ProgressionBalancing, Accessibility +from .options import (Goal, StartingMoney, ProfitMargin, BundleRandomization, BundlePrice, + EntranceRandomization, SeasonRandomization, Cropsanity, BackpackProgression, + ToolProgression, ElevatorProgression, SkillProgression, BuildingProgression, + FestivalLocations, ArcadeMachineLocations, SpecialOrderLocations, + QuestLocations, Fishsanity, Museumsanity, Friendsanity, FriendsanityHeartSize, + NumberOfMovementBuffs, NumberOfLuckBuffs, ExcludeGingerIsland, TrapItems, + MultipleDaySleepEnabled, MultipleDaySleepCost, ExperienceMultiplier, + FriendshipMultiplier, DebrisMultiplier, QuickStart, Gifting, FarmType, + Monstersanity, Shipsanity, Cooksanity, Chefsanity, Craftsanity, Mods) + +sv_option_groups = [ + OptionGroup("General", [ + Goal, + FarmType, + BundleRandomization, + BundlePrice, + EntranceRandomization, + ExcludeGingerIsland, + ]), + OptionGroup("Major Unlocks", [ + SeasonRandomization, + Cropsanity, + BackpackProgression, + ToolProgression, + ElevatorProgression, + SkillProgression, + BuildingProgression, + ]), + OptionGroup("Extra Shuffling", [ + FestivalLocations, + ArcadeMachineLocations, + SpecialOrderLocations, + QuestLocations, + Fishsanity, + Museumsanity, + Friendsanity, + FriendsanityHeartSize, + Monstersanity, + Shipsanity, + Cooksanity, + Chefsanity, + Craftsanity, + ]), + OptionGroup("Multipliers and Buffs", [ + StartingMoney, + ProfitMargin, + ExperienceMultiplier, + FriendshipMultiplier, + DebrisMultiplier, + NumberOfMovementBuffs, + NumberOfLuckBuffs, + TrapItems, + MultipleDaySleepEnabled, + MultipleDaySleepCost, + QuickStart, + ]), + OptionGroup("Advanced Options", [ + Gifting, + DeathLink, + Mods, + ProgressionBalancing, + Accessibility, + ]), +] diff --git a/worlds/stardew_valley/options.py b/worlds/stardew_valley/options.py index 191a634496e4..ba1ebfb9c177 100644 --- a/worlds/stardew_valley/options.py +++ b/worlds/stardew_valley/options.py @@ -697,8 +697,6 @@ class Mods(OptionSet): class StardewValleyOptions(PerGameCommonOptions): goal: Goal farm_type: FarmType - starting_money: StartingMoney - profit_margin: ProfitMargin bundle_randomization: BundleRandomization bundle_price: BundlePrice entrance_randomization: EntranceRandomization @@ -722,16 +720,18 @@ class StardewValleyOptions(PerGameCommonOptions): craftsanity: Craftsanity friendsanity: Friendsanity friendsanity_heart_size: FriendsanityHeartSize + exclude_ginger_island: ExcludeGingerIsland + quick_start: QuickStart + starting_money: StartingMoney + profit_margin: ProfitMargin + experience_multiplier: ExperienceMultiplier + friendship_multiplier: FriendshipMultiplier + debris_multiplier: DebrisMultiplier movement_buff_number: NumberOfMovementBuffs luck_buff_number: NumberOfLuckBuffs - exclude_ginger_island: ExcludeGingerIsland trap_items: TrapItems multiple_day_sleep_enabled: MultipleDaySleepEnabled multiple_day_sleep_cost: MultipleDaySleepCost - experience_multiplier: ExperienceMultiplier - friendship_multiplier: FriendshipMultiplier - debris_multiplier: DebrisMultiplier - quick_start: QuickStart gifting: Gifting mods: Mods death_link: DeathLink From 8b6eae0a1433c0b53cf8d51147460b166b63f017 Mon Sep 17 00:00:00 2001 From: Star Rauchenberger Date: Wed, 22 May 2024 20:22:39 -0400 Subject: [PATCH 11/64] Lingo: Add option groups (#3352) * Lingo: Add option groups * Touched up option docstrings --- worlds/lingo/__init__.py | 3 +- worlds/lingo/options.py | 60 ++++++++++++++++++++++++++++++---------- 2 files changed, 48 insertions(+), 15 deletions(-) diff --git a/worlds/lingo/__init__.py b/worlds/lingo/__init__.py index 113c3928d21c..fa24fdc3bc63 100644 --- a/worlds/lingo/__init__.py +++ b/worlds/lingo/__init__.py @@ -9,12 +9,13 @@ from .datatypes import Room, RoomEntrance from .items import ALL_ITEM_TABLE, ITEMS_BY_GROUP, TRAP_ITEMS, LingoItem from .locations import ALL_LOCATION_TABLE, LOCATIONS_BY_GROUP -from .options import LingoOptions +from .options import LingoOptions, lingo_option_groups from .player_logic import LingoPlayerLogic from .regions import create_regions class LingoWebWorld(WebWorld): + option_groups = lingo_option_groups theme = "grass" tutorials = [Tutorial( "Multiworld Setup Guide", diff --git a/worlds/lingo/options.py b/worlds/lingo/options.py index 65f27269f2c8..1c1f645b8613 100644 --- a/worlds/lingo/options.py +++ b/worlds/lingo/options.py @@ -2,7 +2,8 @@ from schema import And, Schema -from Options import Toggle, Choice, DefaultOnToggle, Range, PerGameCommonOptions, StartInventoryPool, OptionDict +from Options import Toggle, Choice, DefaultOnToggle, Range, PerGameCommonOptions, StartInventoryPool, OptionDict, \ + OptionGroup from .items import TRAP_ITEMS @@ -32,8 +33,8 @@ class ProgressiveColorful(DefaultOnToggle): class LocationChecks(Choice): - """On "normal", there will be a location check for each panel set that would ordinarily open a door, as well as for - achievement panels and a small handful of other panels. + """Determines what locations are available. + On "normal", there will be a location check for each panel set that would ordinarily open a door, as well as for achievement panels and a small handful of other panels. On "reduced", many of the locations that are associated with opening doors are removed. On "insanity", every individual panel in the game is a location check.""" display_name = "Location Checks" @@ -43,8 +44,10 @@ class LocationChecks(Choice): class ShuffleColors(DefaultOnToggle): - """If on, an item is added to the pool for every puzzle color (besides White). - You will need to unlock the requisite colors in order to be able to solve puzzles of that color.""" + """ + If on, an item is added to the pool for every puzzle color (besides White). + You will need to unlock the requisite colors in order to be able to solve puzzles of that color. + """ display_name = "Shuffle Colors" @@ -62,20 +65,25 @@ class ShufflePaintings(Toggle): class EnablePilgrimage(Toggle): - """If on, you are required to complete a pilgrimage in order to access the Pilgrim Antechamber. + """Determines how the pilgrimage works. + If on, you are required to complete a pilgrimage in order to access the Pilgrim Antechamber. If off, the pilgrimage will be deactivated, and the sun painting will be added to the pool, even if door shuffle is off.""" display_name = "Enable Pilgrimage" class PilgrimageAllowsRoofAccess(DefaultOnToggle): - """If on, you may use the Crossroads roof access during a pilgrimage (and you may be expected to do so). - Otherwise, pilgrimage will be deactivated when going up the stairs.""" + """ + If on, you may use the Crossroads roof access during a pilgrimage (and you may be expected to do so). + Otherwise, pilgrimage will be deactivated when going up the stairs. + """ display_name = "Allow Roof Access for Pilgrimage" class PilgrimageAllowsPaintings(DefaultOnToggle): - """If on, you may use paintings during a pilgrimage (and you may be expected to do so). - Otherwise, pilgrimage will be deactivated when going through a painting.""" + """ + If on, you may use paintings during a pilgrimage (and you may be expected to do so). + Otherwise, pilgrimage will be deactivated when going through a painting. + """ display_name = "Allow Paintings for Pilgrimage" @@ -137,8 +145,10 @@ class Level2Requirement(Range): class EarlyColorHallways(Toggle): - """When on, a painting warp to the color hallways area will appear in the starting room. - This lets you avoid being trapped in the starting room for long periods of time when door shuffle is on.""" + """ + When on, a painting warp to the color hallways area will appear in the starting room. + This lets you avoid being trapped in the starting room for long periods of time when door shuffle is on. + """ display_name = "Early Color Hallways" @@ -151,8 +161,10 @@ class TrapPercentage(Range): class TrapWeights(OptionDict): - """Specify the distribution of traps that should be placed into the pool. - If you don't want a specific type of trap, set the weight to zero.""" + """ + Specify the distribution of traps that should be placed into the pool. + If you don't want a specific type of trap, set the weight to zero. + """ display_name = "Trap Weights" schema = Schema({trap_name: And(int, lambda n: n >= 0) for trap_name in TRAP_ITEMS}) default = {trap_name: 1 for trap_name in TRAP_ITEMS} @@ -171,6 +183,26 @@ class DeathLink(Toggle): display_name = "Death Link" +lingo_option_groups = [ + OptionGroup("Pilgrimage", [ + EnablePilgrimage, + PilgrimageAllowsRoofAccess, + PilgrimageAllowsPaintings, + SunwarpAccess, + ShuffleSunwarps, + ]), + OptionGroup("Fine-tuning", [ + ProgressiveOrangeTower, + ProgressiveColorful, + MasteryAchievements, + Level2Requirement, + TrapPercentage, + TrapWeights, + PuzzleSkipPercentage, + ]) +] + + @dataclass class LingoOptions(PerGameCommonOptions): shuffle_doors: ShuffleDoors From e1ff5073b5fbfc378b800b102b83afa7b7be6dcf Mon Sep 17 00:00:00 2001 From: Zach Parks Date: Thu, 23 May 2024 02:08:08 -0500 Subject: [PATCH 12/64] WebHost, Core: Move item and location descriptions to `WebWorld` responsibilities. (#2508) Co-authored-by: Doug Hoskisson Co-authored-by: el-u <109771707+el-u@users.noreply.github.com> --- docs/world api.md | 107 +++++++++++++----------------- test/general/test_items.py | 9 --- test/general/test_locations.py | 9 --- test/webhost/test_descriptions.py | 23 +++++++ worlds/AutoWorld.py | 49 +++----------- worlds/dark_souls_3/Items.py | 6 +- worlds/dark_souls_3/__init__.py | 4 +- 7 files changed, 83 insertions(+), 124 deletions(-) create mode 100644 test/webhost/test_descriptions.py diff --git a/docs/world api.md b/docs/world api.md index 6714fa3a21fb..37638c3c66cc 100644 --- a/docs/world api.md +++ b/docs/world api.md @@ -121,6 +121,53 @@ class RLWeb(WebWorld): # ... ``` +* `location_descriptions` (optional) WebWorlds can provide a map that contains human-friendly descriptions of locations +or location groups. + + ```python + # locations.py + location_descriptions = { + "Red Potion #6": "In a secret destructible block under the second stairway", + "L2 Spaceship": """ + The group of all items in the spaceship in Level 2. + + This doesn't include the item on the spaceship door, since it can be + accessed without the Spaceship Key. + """ + } + + # __init__.py + from worlds.AutoWorld import WebWorld + from .locations import location_descriptions + + + class MyGameWeb(WebWorld): + location_descriptions = location_descriptions + ``` + +* `item_descriptions` (optional) WebWorlds can provide a map that contains human-friendly descriptions of items or item +groups. + + ```python + # items.py + item_descriptions = { + "Red Potion": "A standard health potion", + "Spaceship Key": """ + The key to the spaceship in Level 2. + + This is necessary to get to the Star Realm. + """, + } + + # __init__.py + from worlds.AutoWorld import WebWorld + from .items import item_descriptions + + + class MyGameWeb(WebWorld): + item_descriptions = item_descriptions + ``` + ### MultiWorld Object The `MultiWorld` object references the whole multiworld (all items and locations for all players) and is accessible @@ -178,36 +225,6 @@ Classification is one of `LocationProgressType.DEFAULT`, `PRIORITY` or `EXCLUDED The Fill algorithm will force progression items to be placed at priority locations, giving a higher chance of them being required, and will prevent progression and useful items from being placed at excluded locations. -#### Documenting Locations - -Worlds can optionally provide a `location_descriptions` map which contains human-friendly descriptions of locations and -location groups. These descriptions will show up in location-selection options on the options pages. - -```python -# locations.py - -location_descriptions = { - "Red Potion #6": "In a secret destructible block under the second stairway", - "L2 Spaceship": - """ - The group of all items in the spaceship in Level 2. - - This doesn't include the item on the spaceship door, since it can be accessed without the Spaceship Key. - """ -} -``` - -```python -# __init__.py - -from worlds.AutoWorld import World -from .locations import location_descriptions - - -class MyGameWorld(World): - location_descriptions = location_descriptions -``` - ### Items Items are all things that can "drop" for your game. This may be RPG items like weapons, or technologies you normally @@ -232,36 +249,6 @@ Other classifications include: * `progression_skip_balancing`: the combination of `progression` and `skip_balancing`, i.e., a progression item that will not be moved around by progression balancing; used, e.g., for currency or tokens, to not flood early spheres -#### Documenting Items - -Worlds can optionally provide an `item_descriptions` map which contains human-friendly descriptions of items and item -groups. These descriptions will show up in item-selection options on the options pages. - -```python -# items.py - -item_descriptions = { - "Red Potion": "A standard health potion", - "Spaceship Key": - """ - The key to the spaceship in Level 2. - - This is necessary to get to the Star Realm. - """ -} -``` - -```python -# __init__.py - -from worlds.AutoWorld import World -from .items import item_descriptions - - -class MyGameWorld(World): - item_descriptions = item_descriptions -``` - ### Events An Event is a special combination of a Location and an Item, with both having an `id` of `None`. These can be used to diff --git a/test/general/test_items.py b/test/general/test_items.py index 7c0b7050c670..9cc91a1b00ef 100644 --- a/test/general/test_items.py +++ b/test/general/test_items.py @@ -64,15 +64,6 @@ def test_items_in_datapackage(self): for item in multiworld.itempool: self.assertIn(item.name, world_type.item_name_to_id) - def test_item_descriptions_have_valid_names(self): - """Ensure all item descriptions match an item name or item group name""" - for game_name, world_type in AutoWorldRegister.world_types.items(): - valid_names = world_type.item_names.union(world_type.item_name_groups) - for name in world_type.item_descriptions: - with self.subTest("Name should be valid", game=game_name, item=name): - self.assertIn(name, valid_names, - "All item descriptions must match defined item names") - def test_itempool_not_modified(self): """Test that worlds don't modify the itempool after `create_items`""" gen_steps = ("generate_early", "create_regions", "create_items") diff --git a/test/general/test_locations.py b/test/general/test_locations.py index 2ac059312c17..4b95ebd22c90 100644 --- a/test/general/test_locations.py +++ b/test/general/test_locations.py @@ -66,12 +66,3 @@ def test_location_group(self): for location in locations: self.assertIn(location, world_type.location_name_to_id) self.assertNotIn(group_name, world_type.location_name_to_id) - - def test_location_descriptions_have_valid_names(self): - """Ensure all location descriptions match a location name or location group name""" - for game_name, world_type in AutoWorldRegister.world_types.items(): - valid_names = world_type.location_names.union(world_type.location_name_groups) - for name in world_type.location_descriptions: - with self.subTest("Name should be valid", game=game_name, location=name): - self.assertIn(name, valid_names, - "All location descriptions must match defined location names") diff --git a/test/webhost/test_descriptions.py b/test/webhost/test_descriptions.py new file mode 100644 index 000000000000..70f375b51cf0 --- /dev/null +++ b/test/webhost/test_descriptions.py @@ -0,0 +1,23 @@ +import unittest + +from worlds.AutoWorld import AutoWorldRegister + + +class TestWebDescriptions(unittest.TestCase): + def test_item_descriptions_have_valid_names(self) -> None: + """Ensure all item descriptions match an item name or item group name""" + for game_name, world_type in AutoWorldRegister.world_types.items(): + valid_names = world_type.item_names.union(world_type.item_name_groups) + for name in world_type.web.item_descriptions: + with self.subTest("Name should be valid", game=game_name, item=name): + self.assertIn(name, valid_names, + "All item descriptions must match defined item names") + + def test_location_descriptions_have_valid_names(self) -> None: + """Ensure all location descriptions match a location name or location group name""" + for game_name, world_type in AutoWorldRegister.world_types.items(): + valid_names = world_type.location_names.union(world_type.location_name_groups) + for name in world_type.web.location_descriptions: + with self.subTest("Name should be valid", game=game_name, location=name): + self.assertIn(name, valid_names, + "All location descriptions must match defined location names") diff --git a/worlds/AutoWorld.py b/worlds/AutoWorld.py index b564932eb9b2..32a84f5d577f 100644 --- a/worlds/AutoWorld.py +++ b/worlds/AutoWorld.py @@ -3,13 +3,12 @@ import hashlib import logging import pathlib -from random import Random -import re import sys import time +from random import Random from dataclasses import make_dataclass -from typing import (Any, Callable, ClassVar, Dict, FrozenSet, List, Mapping, - Optional, Set, TextIO, Tuple, TYPE_CHECKING, Type, Union) +from typing import (Any, Callable, ClassVar, Dict, FrozenSet, List, Mapping, Optional, Set, TextIO, Tuple, + TYPE_CHECKING, Type, Union) from Options import ( ExcludeLocations, ItemLinks, LocalItems, NonLocalItems, OptionGroup, PerGameCommonOptions, @@ -55,17 +54,12 @@ def __new__(mcs, name: str, bases: Tuple[type, ...], dct: Dict[str, Any]) -> Aut dct["item_name_groups"] = {group_name: frozenset(group_set) for group_name, group_set in dct.get("item_name_groups", {}).items()} dct["item_name_groups"]["Everything"] = dct["item_names"] - dct["item_descriptions"] = {name: _normalize_description(description) for name, description - in dct.get("item_descriptions", {}).items()} - dct["item_descriptions"]["Everything"] = "All items in the entire game." + dct["location_names"] = frozenset(dct["location_name_to_id"]) dct["location_name_groups"] = {group_name: frozenset(group_set) for group_name, group_set in dct.get("location_name_groups", {}).items()} dct["location_name_groups"]["Everywhere"] = dct["location_names"] dct["all_item_and_group_names"] = frozenset(dct["item_names"] | set(dct.get("item_name_groups", {}))) - dct["location_descriptions"] = {name: _normalize_description(description) for name, description - in dct.get("location_descriptions", {}).items()} - dct["location_descriptions"]["Everywhere"] = "All locations in the entire game." # move away from get_required_client_version function if "game" in dct: @@ -226,6 +220,12 @@ class WebWorld(metaclass=WebWorldRegister): option_groups: ClassVar[List[OptionGroup]] = [] """Ordered list of option groupings. Any options not set in a group will be placed in a pre-built "Game Options".""" + location_descriptions: Dict[str, str] = {} + """An optional map from location names (or location group names) to brief descriptions for users.""" + + item_descriptions: Dict[str, str] = {} + """An optional map from item names (or item group names) to brief descriptions for users.""" + class World(metaclass=AutoWorldRegister): """A World object encompasses a game's Items, Locations, Rules and additional data or functionality required. @@ -252,23 +252,9 @@ class World(metaclass=AutoWorldRegister): item_name_groups: ClassVar[Dict[str, Set[str]]] = {} """maps item group names to sets of items. Example: {"Weapons": {"Sword", "Bow"}}""" - item_descriptions: ClassVar[Dict[str, str]] = {} - """An optional map from item names (or item group names) to brief descriptions for users. - - Individual newlines and indentation will be collapsed into spaces before these descriptions are - displayed. This may cover only a subset of items. - """ - location_name_groups: ClassVar[Dict[str, Set[str]]] = {} """maps location group names to sets of locations. Example: {"Sewer": {"Sewer Key Drop 1", "Sewer Key Drop 2"}}""" - location_descriptions: ClassVar[Dict[str, str]] = {} - """An optional map from location names (or location group names) to brief descriptions for users. - - Individual newlines and indentation will be collapsed into spaces before these descriptions are - displayed. This may cover only a subset of locations. - """ - data_version: ClassVar[int] = 0 """ Increment this every time something in your world's names/id mappings changes. @@ -572,18 +558,3 @@ def data_package_checksum(data: "GamesPackage") -> str: assert sorted(data) == list(data), "Data not ordered" from NetUtils import encode return hashlib.sha1(encode(data).encode()).hexdigest() - - -def _normalize_description(description): - """ - Normalizes a description in item_descriptions or location_descriptions. - - This allows authors to write descritions with nice indentation and line lengths in their world - definitions without having it affect the rendered format. - """ - # First, collapse the whitespace around newlines and the ends of the description. - description = re.sub(r' *\n *', '\n', description.strip()) - # Next, condense individual newlines into spaces. - description = re.sub(r'(? dict: ]] item_descriptions = { - "Cinders": """ - All four Cinders of a Lord. - - Once you have these four, you can fight Soul of Cinder and win the game. - """, + "Cinders": "All four Cinders of a Lord.\n\nOnce you have these four, you can fight Soul of Cinder and win the game.", } _all_items = _vanilla_items + _dlc_items diff --git a/worlds/dark_souls_3/__init__.py b/worlds/dark_souls_3/__init__.py index b4c231cdea1b..c4b2232b32dc 100644 --- a/worlds/dark_souls_3/__init__.py +++ b/worlds/dark_souls_3/__init__.py @@ -35,6 +35,8 @@ class DarkSouls3Web(WebWorld): tutorials = [setup_en, setup_fr] + item_descriptions = item_descriptions + class DarkSouls3World(World): """ @@ -61,8 +63,6 @@ class DarkSouls3World(World): "Cinders of a Lord - Lothric Prince" } } - item_descriptions = item_descriptions - def __init__(self, multiworld: MultiWorld, player: int): super().__init__(multiworld, player) From 3f8c348a49dd2ffeb77805f790b153131b7c63bf Mon Sep 17 00:00:00 2001 From: CookieCat <81494827+CookieCat45@users.noreply.github.com> Date: Thu, 23 May 2024 03:49:17 -0400 Subject: [PATCH 13/64] AHIT: Fix Your Contract has Expired being placed on the first level when it shouldn't (#3379) --- worlds/ahit/Regions.py | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/worlds/ahit/Regions.py b/worlds/ahit/Regions.py index 6a388a98e87e..0ba0f5b9a5a4 100644 --- a/worlds/ahit/Regions.py +++ b/worlds/ahit/Regions.py @@ -699,11 +699,14 @@ def is_valid_first_act(world: "HatInTimeWorld", act: Region) -> bool: # Needs to be at least moderate to cross the big dweller wall if act.name == "Queen Vanessa's Manor" and diff < Difficulty.MODERATE: return False - elif act.name == "Your Contract has Expired" and diff < Difficulty.EXPERT: # Snatcher Hover - return False elif act.name == "Heating Up Mafia Town": # Straight up impossible return False + # Need to be able to hover + if act.name == "Your Contract has Expired": + if diff < Difficulty.EXPERT or world.options.ShuffleSubconPaintings and world.options.NoPaintingSkips: + return False + if act.name == "Dead Bird Studio": # No umbrella logic = moderate, umbrella logic = expert. if diff < Difficulty.MODERATE or world.options.UmbrellaLogic and diff < Difficulty.EXPERT: @@ -718,14 +721,9 @@ def is_valid_first_act(world: "HatInTimeWorld", act: Region) -> bool: return False if world.options.ShuffleSubconPaintings and act_chapters.get(act.name, "") == "Subcon Forest": - # This requires a cherry hover to enter Subcon - if act.name == "Your Contract has Expired": - if diff < Difficulty.EXPERT or world.options.NoPaintingSkips: - return False - else: - # Only allow Subcon levels if paintings can be skipped - if diff < Difficulty.MODERATE or world.options.NoPaintingSkips: - return False + # Only allow Subcon levels if painting skips are allowed + if diff < Difficulty.MODERATE or world.options.NoPaintingSkips: + return False return True From 860ab10b0bb774d7f716eb3e6db15eb4454f2fef Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Thu, 23 May 2024 15:03:21 +0200 Subject: [PATCH 14/64] Generate: remove tag "-" (#3036) * Generate: introduce Remove, similar to Merge * make + dict behave as + for each value --------- Co-authored-by: Zach Parks --- Generate.py | 29 ++++++++++++++++---- test/general/test_player_options.py | 2 +- worlds/generic/docs/triggers_en.md | 42 +++++++++++++++++++---------- 3 files changed, 53 insertions(+), 20 deletions(-) diff --git a/Generate.py b/Generate.py index 2bc061b7468b..30b992317d73 100644 --- a/Generate.py +++ b/Generate.py @@ -9,6 +9,7 @@ import urllib.request from collections import Counter from typing import Any, Dict, Tuple, Union +from itertools import chain import ModuleUpdate @@ -319,18 +320,34 @@ def update_weights(weights: dict, new_weights: dict, update_type: str, name: str logging.debug(f'Applying {new_weights}') cleaned_weights = {} for option in new_weights: - option_name = option.lstrip("+") + option_name = option.lstrip("+-") if option.startswith("+") and option_name in weights: cleaned_value = weights[option_name] new_value = new_weights[option] - if isinstance(new_value, (set, dict)): + if isinstance(new_value, set): cleaned_value.update(new_value) elif isinstance(new_value, list): cleaned_value.extend(new_value) + elif isinstance(new_value, dict): + cleaned_value = dict(Counter(cleaned_value) + Counter(new_value)) else: raise Exception(f"Cannot apply merge to non-dict, set, or list type {option_name}," f" received {type(new_value).__name__}.") cleaned_weights[option_name] = cleaned_value + elif option.startswith("-") and option_name in weights: + cleaned_value = weights[option_name] + new_value = new_weights[option] + if isinstance(new_value, set): + cleaned_value.difference_update(new_value) + elif isinstance(new_value, list): + for element in new_value: + cleaned_value.remove(element) + elif isinstance(new_value, dict): + cleaned_value = dict(Counter(cleaned_value) - Counter(new_value)) + else: + raise Exception(f"Cannot apply remove to non-dict, set, or list type {option_name}," + f" received {type(new_value).__name__}.") + cleaned_weights[option_name] = cleaned_value else: cleaned_weights[option_name] = new_weights[option] new_options = set(cleaned_weights) - set(weights) @@ -466,9 +483,11 @@ def roll_settings(weights: dict, plando_options: PlandoOptions = PlandoOptions.b world_type = AutoWorldRegister.world_types[ret.game] game_weights = weights[ret.game] - if any(weight.startswith("+") for weight in game_weights) or \ - any(weight.startswith("+") for weight in weights): - raise Exception(f"Merge tag cannot be used outside of trigger contexts.") + for weight in chain(game_weights, weights): + if weight.startswith("+"): + raise Exception(f"Merge tag cannot be used outside of trigger contexts. Found {weight}") + if weight.startswith("-"): + raise Exception(f"Remove tag cannot be used outside of trigger contexts. Found {weight}") if "triggers" in game_weights: weights = roll_triggers(weights, game_weights["triggers"], valid_trigger_names) diff --git a/test/general/test_player_options.py b/test/general/test_player_options.py index 9650fbe97a95..ea7f19e3d917 100644 --- a/test/general/test_player_options.py +++ b/test/general/test_player_options.py @@ -31,7 +31,7 @@ def test_update_weights(self): self.assertEqual(new_weights["list_2"], ["string_3"]) self.assertEqual(new_weights["list_1"], ["string", "string_2"]) self.assertEqual(new_weights["dict_1"]["option_a"], 50) - self.assertEqual(new_weights["dict_1"]["option_b"], 0) + self.assertEqual(new_weights["dict_1"]["option_b"], 50) self.assertEqual(new_weights["dict_1"]["option_c"], 50) self.assertNotIn("option_f", new_weights["dict_2"]) self.assertEqual(new_weights["dict_2"]["option_g"], 50) diff --git a/worlds/generic/docs/triggers_en.md b/worlds/generic/docs/triggers_en.md index 73cca6654328..c084b53fb1d8 100644 --- a/worlds/generic/docs/triggers_en.md +++ b/worlds/generic/docs/triggers_en.md @@ -123,10 +123,21 @@ again using the new options `normal`, `pupdunk_hard`, and `pupdunk_mystery`, and new weights for 150 and 200. This allows for two more triggers that will only be used for the new `pupdunk_hard` and `pupdunk_mystery` options so that they will only be triggered on "pupdunk AND hard/mystery". -Options that define a list, set, or dict can additionally have the character `+` added to the start of their name, which applies the contents of -the activated trigger to the already present equivalents in the game options. +## Adding or Removing from a List, Set, or Dict Option + +List, set, and dict options can additionally have values added to or removed from itself without overriding the existing +option value by prefixing the option name in the trigger block with `+` (add) or `-` (remove). The exact behavior for +each will depend on the option type. + +- For sets, `+` will add the value(s) to the set and `-` will remove any value(s) of the set. Sets do not allow + duplicates. +- For lists, `+` will add new values(s) to the list and `-` will remove the first matching values(s) it comes across. + Lists allow duplicate values. +- For dicts, `+` will add the value(s) to the given key(s) inside the dict if it exists, or add it otherwise. `-` is the + inverse operation of addition (and negative values are allowed). For example: + ```yaml Super Metroid: start_location: @@ -134,18 +145,21 @@ Super Metroid: aqueduct: 50 start_hints: - Morph Ball -triggers: - - option_category: Super Metroid - option_name: start_location - option_result: aqueduct - options: - Super Metroid: - +start_hints: - - Gravity Suit + start_inventory: + Power Bombs: 1 + triggers: + - option_category: Super Metroid + option_name: start_location + option_result: aqueduct + options: + Super Metroid: + +start_hints: + - Gravity Suit ``` -In this example, if the `start_location` option rolls `landing_site`, only a starting hint for Morph Ball will be created. -If `aqueduct` is rolled, a starting hint for Gravity Suit will also be created alongside the hint for Morph Ball. +In this example, if the `start_location` option rolls `landing_site`, only a starting hint for Morph Ball will be +created. If `aqueduct` is rolled, a starting hint for Gravity Suit will also be created alongside the hint for Morph +Ball. -Note that for lists, items can only be added, not removed or replaced. For dicts, defining a value for a present key will -replace that value within the dict. +Note that for lists, items can only be added, not removed or replaced. For dicts, defining a value for a present key +will replace that value within the dict. From d09b2143092cdc3716a7042135f9e9689bb20b36 Mon Sep 17 00:00:00 2001 From: Doug Hoskisson Date: Thu, 23 May 2024 10:55:45 -0700 Subject: [PATCH 15/64] Core: Utils.py typing (#3064) * Core: Utils.py typing `get_fuzzy_results` typing There are places that this is called with a `word_list` that is not a `Sequence`, and it is valid (e.g., `set` or `dict`). To decide the right type, we look at how `word_list` is used: - the parameter to `len` - requires `__len__` - the 2nd parameter to `map` - requires `__iter__` Then we look at https://docs.python.org/3/library/collections.abc.html#collections-abstract-base-classes and ask what is the simplest type that includes both `__len__` and `__iter__`: `Collection` (Python 3.8 requires using the alias in `typing`, instead of `collections.abc`) * a bit more typing and cleaning * fine, take away my fun for something that no one is ever going to see anyway... Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com> --------- Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com> --- Utils.py | 56 +++++++++++++++++++++++++++++--------------------------- 1 file changed, 29 insertions(+), 27 deletions(-) diff --git a/Utils.py b/Utils.py index 141b1dc7f8c6..780271996583 100644 --- a/Utils.py +++ b/Utils.py @@ -101,8 +101,7 @@ def cache_self1(function: typing.Callable[[S, T], RetType]) -> typing.Callable[[ @functools.wraps(function) def wrap(self: S, arg: T) -> RetType: - cache: Optional[Dict[T, RetType]] = typing.cast(Optional[Dict[T, RetType]], - getattr(self, cache_name, None)) + cache: Optional[Dict[T, RetType]] = getattr(self, cache_name, None) if cache is None: res = function(self, arg) setattr(self, cache_name, {arg: res}) @@ -209,10 +208,11 @@ def output_path(*path: str) -> str: def open_file(filename: typing.Union[str, "pathlib.Path"]) -> None: if is_windows: - os.startfile(filename) + os.startfile(filename) # type: ignore else: from shutil import which open_command = which("open") if is_macos else (which("xdg-open") or which("gnome-open") or which("kde-open")) + assert open_command, "Didn't find program for open_file! Please report this together with system details." subprocess.call([open_command, filename]) @@ -300,21 +300,21 @@ def get_options() -> Settings: return get_settings() -def persistent_store(category: str, key: typing.Any, value: typing.Any): +def persistent_store(category: str, key: str, value: typing.Any): path = user_path("_persistent_storage.yaml") - storage: dict = persistent_load() - category = storage.setdefault(category, {}) - category[key] = value + storage = persistent_load() + category_dict = storage.setdefault(category, {}) + category_dict[key] = value with open(path, "wt") as f: f.write(dump(storage, Dumper=Dumper)) -def persistent_load() -> typing.Dict[str, dict]: - storage = getattr(persistent_load, "storage", None) +def persistent_load() -> Dict[str, Dict[str, Any]]: + storage: Union[Dict[str, Dict[str, Any]], None] = getattr(persistent_load, "storage", None) if storage: return storage path = user_path("_persistent_storage.yaml") - storage: dict = {} + storage = {} if os.path.exists(path): try: with open(path, "r") as f: @@ -323,7 +323,7 @@ def persistent_load() -> typing.Dict[str, dict]: logging.debug(f"Could not read store: {e}") if storage is None: storage = {} - persistent_load.storage = storage + setattr(persistent_load, "storage", storage) return storage @@ -365,6 +365,7 @@ def store_data_package_for_checksum(game: str, data: typing.Dict[str, Any]) -> N except Exception as e: logging.debug(f"Could not store data package: {e}") + def get_default_adjuster_settings(game_name: str) -> Namespace: import LttPAdjuster adjuster_settings = Namespace() @@ -383,7 +384,9 @@ def get_adjuster_settings(game_name: str) -> Namespace: default_settings = get_default_adjuster_settings(game_name) # Fill in any arguments from the argparser that we haven't seen before - return Namespace(**vars(adjuster_settings), **{k:v for k,v in vars(default_settings).items() if k not in vars(adjuster_settings)}) + return Namespace(**vars(adjuster_settings), **{ + k: v for k, v in vars(default_settings).items() if k not in vars(adjuster_settings) + }) @cache_argsless @@ -407,13 +410,13 @@ def get_unique_identifier(): class RestrictedUnpickler(pickle.Unpickler): generic_properties_module: Optional[object] - def __init__(self, *args, **kwargs): + def __init__(self, *args: Any, **kwargs: Any) -> None: super(RestrictedUnpickler, self).__init__(*args, **kwargs) self.options_module = importlib.import_module("Options") self.net_utils_module = importlib.import_module("NetUtils") self.generic_properties_module = None - def find_class(self, module, name): + def find_class(self, module: str, name: str) -> type: if module == "builtins" and name in safe_builtins: return getattr(builtins, name) # used by MultiServer -> savegame/multidata @@ -437,7 +440,7 @@ def find_class(self, module, name): raise pickle.UnpicklingError(f"global '{module}.{name}' is forbidden") -def restricted_loads(s): +def restricted_loads(s: bytes) -> Any: """Helper function analogous to pickle.loads().""" return RestrictedUnpickler(io.BytesIO(s)).load() @@ -493,7 +496,7 @@ def init_logging(name: str, loglevel: typing.Union[str, int] = logging.INFO, wri file_handler.setFormatter(logging.Formatter(log_format)) class Filter(logging.Filter): - def __init__(self, filter_name, condition): + def __init__(self, filter_name: str, condition: typing.Callable[[logging.LogRecord], bool]) -> None: super().__init__(filter_name) self.condition = condition @@ -544,7 +547,7 @@ def _cleanup(): ) -def stream_input(stream, queue): +def stream_input(stream: typing.TextIO, queue: "asyncio.Queue[str]"): def queuer(): while 1: try: @@ -572,7 +575,7 @@ class VersionException(Exception): pass -def chaining_prefix(index: int, labels: typing.Tuple[str]) -> str: +def chaining_prefix(index: int, labels: typing.Sequence[str]) -> str: text = "" max_label = len(labels) - 1 while index > max_label: @@ -595,7 +598,7 @@ def format_SI_prefix(value, power=1000, power_labels=("", "k", "M", "G", "T", "P return f"{value.quantize(decimal.Decimal('1.00'))} {chaining_prefix(n, power_labels)}" -def get_fuzzy_results(input_word: str, wordlist: typing.Sequence[str], limit: typing.Optional[int] = None) \ +def get_fuzzy_results(input_word: str, word_list: typing.Collection[str], limit: typing.Optional[int] = None) \ -> typing.List[typing.Tuple[str, int]]: import jellyfish @@ -603,21 +606,20 @@ def get_fuzzy_ratio(word1: str, word2: str) -> float: return (1 - jellyfish.damerau_levenshtein_distance(word1.lower(), word2.lower()) / max(len(word1), len(word2))) - limit: int = limit if limit else len(wordlist) + limit = limit if limit else len(word_list) return list( map( lambda container: (container[0], int(container[1]*100)), # convert up to limit to int % sorted( - map(lambda candidate: - (candidate, get_fuzzy_ratio(input_word, candidate)), - wordlist), + map(lambda candidate: (candidate, get_fuzzy_ratio(input_word, candidate)), word_list), key=lambda element: element[1], - reverse=True)[0:limit] + reverse=True + )[0:limit] ) ) -def open_filename(title: str, filetypes: typing.Sequence[typing.Tuple[str, typing.Sequence[str]]], suggest: str = "") \ +def open_filename(title: str, filetypes: typing.Iterable[typing.Tuple[str, typing.Iterable[str]]], suggest: str = "") \ -> typing.Optional[str]: logging.info(f"Opening file input dialog for {title}.") @@ -734,7 +736,7 @@ def is_kivy_running(): root.update() -def title_sorted(data: typing.Sequence, key=None, ignore: typing.Set = frozenset(("a", "the"))): +def title_sorted(data: typing.Iterable, key=None, ignore: typing.AbstractSet[str] = frozenset(("a", "the"))): """Sorts a sequence of text ignoring typical articles like "a" or "the" in the beginning.""" def sorter(element: Union[str, Dict[str, Any]]) -> str: if (not isinstance(element, str)): @@ -788,7 +790,7 @@ class DeprecateDict(dict): log_message: str should_error: bool - def __init__(self, message, error: bool = False) -> None: + def __init__(self, message: str, error: bool = False) -> None: self.log_message = message self.should_error = error super().__init__() From 8b992cbf00dd69d187cbc9bad0046b0b1421bded Mon Sep 17 00:00:00 2001 From: Aaron Wagener Date: Thu, 23 May 2024 17:50:40 -0500 Subject: [PATCH 16/64] Webhost: Disallow empty option groups (#3369) * move item_and_loc_options out of the meta class and into the Options module * don't allow empty world specified option groups * reuse option_group generation code instead of rewriting it * delete the default group if it's empty * indent --- Options.py | 44 +++++++++++++++++++++++++++++++------------ WebHostLib/options.py | 14 ++------------ worlds/AutoWorld.py | 8 ++------ 3 files changed, 36 insertions(+), 30 deletions(-) diff --git a/Options.py b/Options.py index 39fd56765615..e11e078a1d45 100644 --- a/Options.py +++ b/Options.py @@ -1132,7 +1132,37 @@ class OptionGroup(typing.NamedTuple): """Options to be in the defined group.""" -def generate_yaml_templates(target_folder: typing.Union[str, "pathlib.Path"], generate_hidden: bool = True): +item_and_loc_options = [LocalItems, NonLocalItems, StartInventory, StartInventoryPool, StartHints, + StartLocationHints, ExcludeLocations, PriorityLocations, ItemLinks] +""" +Options that are always populated in "Item & Location Options" Option Group. Cannot be moved to another group. +If desired, a custom "Item & Location Options" Option Group can be defined, but only for adding additional options to +it. +""" + + +def get_option_groups(world: typing.Type[World], visibility_level: Visibility = Visibility.template) -> typing.Dict[ + str, typing.Dict[str, typing.Type[Option[typing.Any]]]]: + """Generates and returns a dictionary for the option groups of a specified world.""" + option_groups = {option: option_group.name + for option_group in world.web.option_groups + for option in option_group.options} + # add a default option group for uncategorized options to get thrown into + ordered_groups = ["Game Options"] + [ordered_groups.append(group) for group in option_groups.values() if group not in ordered_groups] + grouped_options = {group: {} for group in ordered_groups} + for option_name, option in world.options_dataclass.type_hints.items(): + if visibility_level & option.visibility: + grouped_options[option_groups.get(option, "Game Options")][option_name] = option + + # if the world doesn't have any ungrouped options, this group will be empty so just remove it + if not grouped_options["Game Options"]: + del grouped_options["Game Options"] + + return grouped_options + + +def generate_yaml_templates(target_folder: typing.Union[str, "pathlib.Path"], generate_hidden: bool = True) -> None: import os import yaml @@ -1170,17 +1200,7 @@ def dictify_range(option: Range): for game_name, world in AutoWorldRegister.world_types.items(): if not world.hidden or generate_hidden: - - option_groups = {option: option_group.name - for option_group in world.web.option_groups - for option in option_group.options} - ordered_groups = ["Game Options"] - [ordered_groups.append(group) for group in option_groups.values() if group not in ordered_groups] - grouped_options = {group: {} for group in ordered_groups} - for option_name, option in world.options_dataclass.type_hints.items(): - if option.visibility >= Visibility.template: - grouped_options[option_groups.get(option, "Game Options")][option_name] = option - + grouped_options = get_option_groups(world) with open(local_path("data", "options.yaml")) as f: file_data = f.read() res = Template(file_data).render( diff --git a/WebHostLib/options.py b/WebHostLib/options.py index 94f173df70cb..bc63ec93318a 100644 --- a/WebHostLib/options.py +++ b/WebHostLib/options.py @@ -27,26 +27,16 @@ def get_world_theme(game_name: str) -> str: def render_options_page(template: str, world_name: str, is_complex: bool = False) -> Union[Response, str]: - visibility_flag = Options.Visibility.complex_ui if is_complex else Options.Visibility.simple_ui world = AutoWorldRegister.world_types[world_name] if world.hidden or world.web.options_page is False: return redirect("games") - - option_groups = {option: option_group.name - for option_group in world.web.option_groups - for option in option_group.options} - ordered_groups = ["Game Options", *[group.name for group in world.web.option_groups]] - grouped_options = {group: {} for group in ordered_groups} - for option_name, option in world.options_dataclass.type_hints.items(): - # Exclude settings from options pages if their visibility is disabled - if visibility_flag in option.visibility: - grouped_options[option_groups.get(option, "Game Options")][option_name] = option + visibility_flag = Options.Visibility.complex_ui if is_complex else Options.Visibility.simple_ui return render_template( template, world_name=world_name, world=world, - option_groups=grouped_options, + option_groups=Options.get_option_groups(world, visibility_level=visibility_flag), issubclass=issubclass, Options=Options, theme=get_world_theme(world_name), diff --git a/worlds/AutoWorld.py b/worlds/AutoWorld.py index 32a84f5d577f..f8bc525ea57b 100644 --- a/worlds/AutoWorld.py +++ b/worlds/AutoWorld.py @@ -10,10 +10,7 @@ from typing import (Any, Callable, ClassVar, Dict, FrozenSet, List, Mapping, Optional, Set, TextIO, Tuple, TYPE_CHECKING, Type, Union) -from Options import ( - ExcludeLocations, ItemLinks, LocalItems, NonLocalItems, OptionGroup, PerGameCommonOptions, - PriorityLocations, StartHints, StartInventory, StartInventoryPool, StartLocationHints -) +from Options import item_and_loc_options, OptionGroup, PerGameCommonOptions from BaseClasses import CollectionState if TYPE_CHECKING: @@ -119,12 +116,11 @@ def __new__(mcs, name: str, bases: Tuple[type, ...], dct: Dict[str, Any]) -> Web # don't allow an option to appear in multiple groups, allow "Item & Location Options" to appear anywhere by the # dev, putting it at the end if they don't define options in it option_groups: List[OptionGroup] = dct.get("option_groups", []) - item_and_loc_options = [LocalItems, NonLocalItems, StartInventory, StartInventoryPool, StartHints, - StartLocationHints, ExcludeLocations, PriorityLocations, ItemLinks] seen_options = [] item_group_in_list = False for group in option_groups: assert group.name != "Game Options", "Game Options is a pre-determined group and can not be defined." + assert group.options, "A custom defined Option Group must contain at least one Option." if group.name == "Item & Location Options": group.options.extend(item_and_loc_options) item_group_in_list = True From 2a47f03e725607319e506d52fc0c3a492fe96155 Mon Sep 17 00:00:00 2001 From: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> Date: Thu, 23 May 2024 20:36:45 -0400 Subject: [PATCH 17/64] Docs: Update trigger guide and advanced yaml guide (#3385) * I guess these don't exist anymore * Update worlds/generic/docs/advanced_settings_en.md Co-authored-by: Scipio Wright --------- Co-authored-by: Scipio Wright --- worlds/generic/docs/advanced_settings_en.md | 12 ++++++------ worlds/generic/docs/triggers_en.md | 5 +---- 2 files changed, 7 insertions(+), 10 deletions(-) diff --git a/worlds/generic/docs/advanced_settings_en.md b/worlds/generic/docs/advanced_settings_en.md index 5b1b583e61b6..37467eeb468e 100644 --- a/worlds/generic/docs/advanced_settings_en.md +++ b/worlds/generic/docs/advanced_settings_en.md @@ -79,7 +79,7 @@ are `description`, `name`, `game`, `requires`, and the name of the games you wan different weights. * `requires` details different requirements from the generator for the YAML to work as you expect it to. Generally this - is good for detailing the version of Archipelago this YAML was prepared for as, if it is rolled on an older version, + is good for detailing the version of Archipelago this YAML was prepared for. If it is rolled on an older version, options may be missing and as such it will not work as expected. If any plando is used in the file then requiring it here to ensure it will be used is good practice. @@ -137,7 +137,7 @@ guide: [Archipelago Plando Guide](/tutorial/Archipelago/plando/en) 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 + can also have local and non-local items, forcing the items to either be placed within the worlds of the group or in worlds outside the group. If players have a varying amount of a specific item in the link, the lowest amount from the players will be the amount put into the group. @@ -277,7 +277,7 @@ one file, removing the need to manage separate files if one chooses to do so. As a precautionary measure, before submitting a multi-game yaml like this one in a synchronous/sync multiworld, please confirm that the other players in the multi are OK with what you are submitting, and please be fairly reasonable about -the submission. (ie. Multiple long games (SMZ3, OoT, HK, etc.) for a game intended to be <2 hrs is not likely considered +the submission. (i.e. Multiple long games (SMZ3, OoT, HK, etc.) for a game intended to be <2 hrs is not likely considered reasonable, but submitting a ChecksFinder alongside another game OR submitting multiple Slay the Spire runs is likely OK) @@ -295,7 +295,7 @@ requires: version: 0.3.2 Super Mario 64: progression_balancing: 50 - accessibilty: items + accessibility: items EnableCoinStars: false StrictCapRequirements: true StrictCannonRequirements: true @@ -315,7 +315,7 @@ name: Minecraft game: Minecraft Minecraft: progression_balancing: 50 - accessibilty: items + accessibility: items advancement_goal: 40 combat_difficulty: hard include_hard_advancements: false @@ -341,7 +341,7 @@ game: ChecksFinder ChecksFinder: progression_balancing: 50 - accessibilty: items + accessibility: items ``` The above example will generate 3 worlds - one Super Mario 64, one Minecraft, and one ChecksFinder. diff --git a/worlds/generic/docs/triggers_en.md b/worlds/generic/docs/triggers_en.md index c084b53fb1d8..b751b8a3ec01 100644 --- a/worlds/generic/docs/triggers_en.md +++ b/worlds/generic/docs/triggers_en.md @@ -129,7 +129,7 @@ List, set, and dict options can additionally have values added to or removed fro option value by prefixing the option name in the trigger block with `+` (add) or `-` (remove). The exact behavior for each will depend on the option type. -- For sets, `+` will add the value(s) to the set and `-` will remove any value(s) of the set. Sets do not allow +- For sets, `+` will add the value(s) to the set and `-` will remove the value(s) from the set. Sets do not allow duplicates. - For lists, `+` will add new values(s) to the list and `-` will remove the first matching values(s) it comes across. Lists allow duplicate values. @@ -160,6 +160,3 @@ Super Metroid: In this example, if the `start_location` option rolls `landing_site`, only a starting hint for Morph Ball will be created. If `aqueduct` is rolled, a starting hint for Gravity Suit will also be created alongside the hint for Morph Ball. - -Note that for lists, items can only be added, not removed or replaced. For dicts, defining a value for a present key -will replace that value within the dict. From 613e76689e128ad66e37a9b443615a76f6ddd20d Mon Sep 17 00:00:00 2001 From: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com> Date: Fri, 24 May 2024 03:25:41 +0200 Subject: [PATCH 18/64] CODEOWNERS: Actually link the correct person for Yu Gi Oh (#3389) --- docs/CODEOWNERS | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/CODEOWNERS b/docs/CODEOWNERS index c34046d5dc30..068c7240578d 100644 --- a/docs/CODEOWNERS +++ b/docs/CODEOWNERS @@ -207,7 +207,7 @@ /worlds/yoshisisland/ @PinkSwitch #Yu-Gi-Oh! Ultimate Masters: World Championship Tournament 2006 -/worlds/yugioh06/ @rensen +/worlds/yugioh06/ @Rensen3 # Zillion /worlds/zillion/ @beauxq From 8045c8717c645dbc000402e81fd08b8cd41536bd Mon Sep 17 00:00:00 2001 From: Aaron Wagener Date: Fri, 24 May 2024 00:18:21 -0500 Subject: [PATCH 19/64] Webhost: Allow Option Groups to specify whether they start collapsed (#3370) * allow option groups to specify whether they should be hidden or not * allow worlds to override whether game options starts collapsed * remove Game Options assert so the visibility of that group can be changed * if "Game Options" or "Item & Location Options" groups are specified, fix casing * don't allow item & location options to have duplicates of the auto added options * use a generator instead of a comprehension * use consistent naming --- Options.py | 2 ++ WebHostLib/options.py | 5 +++++ WebHostLib/templates/playerOptions/playerOptions.html | 2 +- .../templates/weightedOptions/weightedOptions.html | 2 +- worlds/AutoWorld.py | 11 +++++++++-- 5 files changed, 18 insertions(+), 4 deletions(-) diff --git a/Options.py b/Options.py index e11e078a1d45..7f480cbaae8d 100644 --- a/Options.py +++ b/Options.py @@ -1130,6 +1130,8 @@ class OptionGroup(typing.NamedTuple): """Name of the group to categorize these options in for display on the WebHost and in generated YAMLS.""" options: typing.List[typing.Type[Option[typing.Any]]] """Options to be in the defined group.""" + start_collapsed: bool = False + """Whether the group will start collapsed on the WebHost options pages.""" item_and_loc_options = [LocalItems, NonLocalItems, StartInventory, StartInventoryPool, StartHints, diff --git a/WebHostLib/options.py b/WebHostLib/options.py index bc63ec93318a..4a791135d7c6 100644 --- a/WebHostLib/options.py +++ b/WebHostLib/options.py @@ -32,11 +32,16 @@ def render_options_page(template: str, world_name: str, is_complex: bool = False return redirect("games") visibility_flag = Options.Visibility.complex_ui if is_complex else Options.Visibility.simple_ui + start_collapsed = {"Game Options": False} + for group in world.web.option_groups: + start_collapsed[group.name] = group.start_collapsed + return render_template( template, world_name=world_name, world=world, option_groups=Options.get_option_groups(world, visibility_level=visibility_flag), + start_collapsed=start_collapsed, issubclass=issubclass, Options=Options, theme=get_world_theme(world_name), diff --git a/WebHostLib/templates/playerOptions/playerOptions.html b/WebHostLib/templates/playerOptions/playerOptions.html index 56576109149f..2506cf9619e6 100644 --- a/WebHostLib/templates/playerOptions/playerOptions.html +++ b/WebHostLib/templates/playerOptions/playerOptions.html @@ -69,7 +69,7 @@

Player Options

{% for group_name, group_options in option_groups.items() %} -
+
{{ group_name }}
diff --git a/WebHostLib/templates/weightedOptions/weightedOptions.html b/WebHostLib/templates/weightedOptions/weightedOptions.html index c21671a86385..b3aefd483535 100644 --- a/WebHostLib/templates/weightedOptions/weightedOptions.html +++ b/WebHostLib/templates/weightedOptions/weightedOptions.html @@ -51,7 +51,7 @@

Weighted Options

{% for group_name, group_options in option_groups.items() %} -
+
{{ group_name }} {% for option_name, option in group_options.items() %}
diff --git a/worlds/AutoWorld.py b/worlds/AutoWorld.py index f8bc525ea57b..5d674c0c22fd 100644 --- a/worlds/AutoWorld.py +++ b/worlds/AutoWorld.py @@ -116,12 +116,19 @@ def __new__(mcs, name: str, bases: Tuple[type, ...], dct: Dict[str, Any]) -> Web # don't allow an option to appear in multiple groups, allow "Item & Location Options" to appear anywhere by the # dev, putting it at the end if they don't define options in it option_groups: List[OptionGroup] = dct.get("option_groups", []) + prebuilt_options = ["Game Options", "Item & Location Options"] seen_options = [] item_group_in_list = False for group in option_groups: - assert group.name != "Game Options", "Game Options is a pre-determined group and can not be defined." assert group.options, "A custom defined Option Group must contain at least one Option." + # catch incorrectly titled versions of the prebuilt groups so they don't create extra groups + title_name = group.name.title() + if title_name in prebuilt_options: + group.name = title_name + if group.name == "Item & Location Options": + assert not any(option in item_and_loc_options for option in group.options), \ + f"Item and Location Options cannot be specified multiple times" group.options.extend(item_and_loc_options) item_group_in_list = True else: @@ -133,7 +140,7 @@ def __new__(mcs, name: str, bases: Tuple[type, ...], dct: Dict[str, Any]) -> Web assert option not in seen_options, f"{option} found in two option groups" seen_options.append(option) if not item_group_in_list: - option_groups.append(OptionGroup("Item & Location Options", item_and_loc_options)) + option_groups.append(OptionGroup("Item & Location Options", item_and_loc_options, True)) return super().__new__(mcs, name, bases, dct) From 18390ecc09ad13d94767091675801e4164cdf3ea Mon Sep 17 00:00:00 2001 From: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> Date: Fri, 24 May 2024 13:32:23 -0400 Subject: [PATCH 20/64] Witness: Fix option description (#3396) * Fixing description * Another mistake --- worlds/witness/options.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/worlds/witness/options.py b/worlds/witness/options.py index 00b58ab86959..f51d86ba22f3 100644 --- a/worlds/witness/options.py +++ b/worlds/witness/options.py @@ -76,8 +76,8 @@ class DoorGroupings(Choice): """ Controls how door items are grouped. - - None: There will be one key for each door, potentially resulting in upwards of 120 keys being added to the item pool. - - Regional: - All doors in the same general region will open at once with a single key, reducing the amount of door items and complexity. + - Off: There will be one key for each door, potentially resulting in upwards of 120 keys being added to the item pool. + - Regional: All doors in the same general region will open at once with a single key, reducing the amount of door items and complexity. """ display_name = "Door Groupings" option_off = 0 From 61e88526cf4ab8cce380ddf2bb5ce06a7dee6151 Mon Sep 17 00:00:00 2001 From: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com> Date: Sat, 25 May 2024 13:14:13 +0200 Subject: [PATCH 21/64] Core: Rename "count_exclusive" methods to "count_unique" (#3386) * rename exclusive to unique * lint * group as well --- BaseClasses.py | 8 +- worlds/bomb_rush_cyberfunk/Rules.py | 22 ++-- worlds/yugioh06/rules.py | 164 ++++++++++++++-------------- 3 files changed, 97 insertions(+), 97 deletions(-) diff --git a/BaseClasses.py b/BaseClasses.py index ada18f1e1d04..88857f803212 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -728,7 +728,7 @@ def has_from_list(self, items: Iterable[str], player: int, count: int) -> bool: return True return False - def has_from_list_exclusive(self, items: Iterable[str], player: int, count: int) -> bool: + def has_from_list_unique(self, items: Iterable[str], player: int, count: int) -> bool: """Returns True if the state contains at least `count` items matching any of the item names from a list. Ignores duplicates of the same item.""" found: int = 0 @@ -743,7 +743,7 @@ def count_from_list(self, items: Iterable[str], player: int) -> int: """Returns the cumulative count of items from a list present in state.""" return sum(self.prog_items[player][item_name] for item_name in items) - def count_from_list_exclusive(self, items: Iterable[str], player: int) -> int: + def count_from_list_unique(self, items: Iterable[str], player: int) -> int: """Returns the cumulative count of items from a list present in state. Ignores duplicates of the same item.""" return sum(self.prog_items[player][item_name] > 0 for item_name in items) @@ -758,7 +758,7 @@ def has_group(self, item_name_group: str, player: int, count: int = 1) -> bool: return True return False - def has_group_exclusive(self, item_name_group: str, player: int, count: int = 1) -> bool: + def has_group_unique(self, item_name_group: str, player: int, count: int = 1) -> bool: """Returns True if the state contains at least `count` items present in a specified item group. Ignores duplicates of the same item. """ @@ -778,7 +778,7 @@ def count_group(self, item_name_group: str, player: int) -> int: for item_name in self.multiworld.worlds[player].item_name_groups[item_name_group] ) - def count_group_exclusive(self, item_name_group: str, player: int) -> int: + def count_group_unique(self, item_name_group: str, player: int) -> int: """Returns the cumulative count of items from an item group present in state. Ignores duplicates of the same item.""" player_prog_items = self.prog_items[player] diff --git a/worlds/bomb_rush_cyberfunk/Rules.py b/worlds/bomb_rush_cyberfunk/Rules.py index 6f31882cb191..f59a4285709d 100644 --- a/worlds/bomb_rush_cyberfunk/Rules.py +++ b/worlds/bomb_rush_cyberfunk/Rules.py @@ -5,17 +5,17 @@ def graffitiM(state: CollectionState, player: int, limit: bool, spots: int) -> bool: - return state.count_group_exclusive("graffitim", player) * 7 >= spots if limit \ + return state.count_group_unique("graffitim", player) * 7 >= spots if limit \ else state.has_group("graffitim", player) def graffitiL(state: CollectionState, player: int, limit: bool, spots: int) -> bool: - return state.count_group_exclusive("graffitil", player) * 6 >= spots if limit \ + return state.count_group_unique("graffitil", player) * 6 >= spots if limit \ else state.has_group("graffitil", player) def graffitiXL(state: CollectionState, player: int, limit: bool, spots: int) -> bool: - return state.count_group_exclusive("graffitixl", player) * 4 >= spots if limit \ + return state.count_group_unique("graffitixl", player) * 4 >= spots if limit \ else state.has_group("graffitixl", player) @@ -469,7 +469,7 @@ def spots_s_glitchless(state: CollectionState, player: int, limit: bool, access_ break if limit: - sprayable: int = 5 + (state.count_group_exclusive("characters", player) * 5) + sprayable: int = 5 + (state.count_group_unique("characters", player) * 5) if total <= sprayable: return total else: @@ -492,7 +492,7 @@ def spots_s_glitched(state: CollectionState, player: int, limit: bool, access_ca break if limit: - sprayable: int = 5 + (state.count_group_exclusive("characters", player) * 5) + sprayable: int = 5 + (state.count_group_unique("characters", player) * 5) if total <= sprayable: return total else: @@ -537,7 +537,7 @@ def spots_m_glitchless(state: CollectionState, player: int, limit: bool, access_ break if limit: - sprayable: int = state.count_group_exclusive("graffitim", player) * 7 + sprayable: int = state.count_group_unique("graffitim", player) * 7 if total <= sprayable: return total else: @@ -563,7 +563,7 @@ def spots_m_glitched(state: CollectionState, player: int, limit: bool, access_ca break if limit: - sprayable: int = state.count_group_exclusive("graffitim", player) * 7 + sprayable: int = state.count_group_unique("graffitim", player) * 7 if total <= sprayable: return total else: @@ -614,7 +614,7 @@ def spots_l_glitchless(state: CollectionState, player: int, limit: bool, access_ break if limit: - sprayable: int = state.count_group_exclusive("graffitil", player) * 6 + sprayable: int = state.count_group_unique("graffitil", player) * 6 if total <= sprayable: return total else: @@ -641,7 +641,7 @@ def spots_l_glitched(state: CollectionState, player: int, limit: bool, access_ca break if limit: - sprayable: int = state.count_group_exclusive("graffitil", player) * 6 + sprayable: int = state.count_group_unique("graffitil", player) * 6 if total <= sprayable: return total else: @@ -685,7 +685,7 @@ def spots_xl_glitchless(state: CollectionState, player: int, limit: bool, access break if limit: - sprayable: int = state.count_group_exclusive("graffitixl", player) * 4 + sprayable: int = state.count_group_unique("graffitixl", player) * 4 if total <= sprayable: return total else: @@ -712,7 +712,7 @@ def spots_xl_glitched(state: CollectionState, player: int, limit: bool, access_c break if limit: - sprayable: int = state.count_group_exclusive("graffitixl", player) * 4 + sprayable: int = state.count_group_unique("graffitixl", player) * 4 if total <= sprayable: return total else: diff --git a/worlds/yugioh06/rules.py b/worlds/yugioh06/rules.py index 53ea95b27b7c..a804c7e7286a 100644 --- a/worlds/yugioh06/rules.py +++ b/worlds/yugioh06/rules.py @@ -154,11 +154,11 @@ def set_rules(world): lambda state: state.has_all(["Yata-Garasu", "Chaos Emperor Dragon - Envoy of the End", "Sangan"], player) and state.has_any(["No Banlist", "Banlist September 2003"], player), "Can Stall with Monsters": - lambda state: state.count_from_list_exclusive( + lambda state: state.count_from_list_unique( ["Spirit Reaper", "Giant Germ", "Marshmallon", "Nimble Momonga"], player) >= 2, "Can Stall with ST": - lambda state: state.count_from_list_exclusive(["Level Limit - Area B", "Gravity Bind", "Messenger of Peace"], - player) >= 2, + lambda state: state.count_from_list_unique(["Level Limit - Area B", "Gravity Bind", "Messenger of Peace"], + player) >= 2, "Has Back-row removal": lambda state: back_row_removal(state, player) @@ -201,8 +201,8 @@ def set_rules(world): lambda state: yugioh06_difficulty(state, player, 3), "LD18 Attacks forbidden": lambda state: state.has_all(["Wave-Motion Cannon", "Stealth Bird"], player) - and state.count_from_list_exclusive(["Dark World Lightning", "Nobleman of Crossout", - "Shield Crash", "Tribute to the Doomed"], player) >= 2 + and state.count_from_list_unique(["Dark World Lightning", "Nobleman of Crossout", + "Shield Crash", "Tribute to the Doomed"], player) >= 2 and yugioh06_difficulty(state, player, 3), "LD19 All except E-Hero's forbidden": lambda state: state.has_any(["Polymerization", "Fusion Gate"], player) and @@ -363,7 +363,7 @@ def set_rules(world): "TD30 Tribute Summon": lambda state: state.has("Treeborn Frog", player) and yugioh06_difficulty(state, player, 2), "TD31 Special Summon C": - lambda state: state.count_from_list_exclusive( + lambda state: state.count_from_list_unique( ["Aqua Spirit", "Rock Spirit", "Spirit of Flames", "Garuda the Wind Spirit", "Gigantes", "Inferno", "Megarock Dragon", "Silpheed"], player) > 4 and yugioh06_difficulty(state, player, 3), @@ -393,11 +393,11 @@ def set_rules(world): and yugioh06_difficulty(state, player, 3), "TD39 Raviel, Lord of Phantasms": lambda state: state.has_all(["Raviel, Lord of Phantasms", "Giant Germ"], player) and - state.count_from_list_exclusive(["Archfiend Soldier", - "Skull Descovery Knight", - "Slate Warrior", - "D. D. Trainer", - "Earthbound Spirit"], player) >= 3 + state.count_from_list_unique(["Archfiend Soldier", + "Skull Descovery Knight", + "Slate Warrior", + "D. D. Trainer", + "Earthbound Spirit"], player) >= 3 and yugioh06_difficulty(state, player, 3), "TD40 Make a Chain": lambda state: state.has("Ultimate Offering", player) @@ -450,20 +450,20 @@ def set_rules(world): def only_light(state, player): - return state.has_from_list_exclusive([ + return state.has_from_list_unique([ "Dunames Dark Witch", "X-Head Cannon", "Homunculus the Alchemic Being", "Hysteric Fairy", "Ninja Grandmaster Sasuke"], player, 2)\ - and state.has_from_list_exclusive([ + and state.has_from_list_unique([ "Chaos Command Magician", "Cybernetic Magician", "Kaiser Glider", "The Agent of Judgment - Saturn", "Zaborg the Thunder Monarch", "Cyber Dragon"], player, 1) \ - and state.has_from_list_exclusive([ + and state.has_from_list_unique([ "D.D. Warrior Lady", "Mystic Swordsman LV2", "Y-Dragon Head", @@ -472,7 +472,7 @@ def only_light(state, player): def only_dark(state, player): - return state.has_from_list_exclusive([ + return state.has_from_list_unique([ "Dark Elf", "Archfiend Soldier", "Mad Dog of Darkness", @@ -501,7 +501,7 @@ def only_dark(state, player): "Jinzo", "Ryu Kokki" ], player) \ - and state.has_from_list_exclusive([ + and state.has_from_list_unique([ "Legendary Fiend", "Don Zaloog", "Newdoria", @@ -512,7 +512,7 @@ def only_dark(state, player): def only_earth(state, player): - return state.has_from_list_exclusive([ + return state.has_from_list_unique([ "Berserk Gorilla", "Gemini Elf", "Insect Knight", @@ -527,7 +527,7 @@ def only_earth(state, player): "Granmarg the Rock Monarch", "Hieracosphinx", "Saber Beetle" - ], player) and state.has_from_list_exclusive([ + ], player) and state.has_from_list_unique([ "Hyper Hammerhead", "Green Gadget", "Red Gadget", @@ -539,7 +539,7 @@ def only_earth(state, player): def only_water(state, player): - return state.has_from_list_exclusive([ + return state.has_from_list_unique([ "Gagagigo", "Familiar-Possessed - Eria", "7 Colored Fish", @@ -550,7 +550,7 @@ def only_water(state, player): "Amphibian Beast", "Terrorking Salmon", "Mobius the Frost Monarch" - ], player) and state.has_from_list_exclusive([ + ], player) and state.has_from_list_unique([ "Revival Jam", "Yomi Ship", "Treeborn Frog" @@ -558,7 +558,7 @@ def only_water(state, player): def only_fire(state, player): - return state.has_from_list_exclusive([ + return state.has_from_list_unique([ "Blazing Inpachi", "Familiar-Possessed - Hiita", "Great Angus", @@ -566,7 +566,7 @@ def only_fire(state, player): ], player, 2) and state.has_any([ "Thestalos the Firestorm Monarch", "Horus the Black Flame Dragon LV6" - ], player) and state.has_from_list_exclusive([ + ], player) and state.has_from_list_unique([ "Solar Flare Dragon", "Tenkabito Shien", "Ultimate Baseball Kid" @@ -574,7 +574,7 @@ def only_fire(state, player): def only_wind(state, player): - return state.has_from_list_exclusive([ + return state.has_from_list_unique([ "Luster Dragon", "Slate Warrior", "Spear Dragon", @@ -588,7 +588,7 @@ def only_wind(state, player): "Luster Dragon #2", "Armed Dragon LV5", "Roc from the Valley of Haze" - ], player) and state.has_from_list_exclusive([ + ], player) and state.has_from_list_unique([ "Armed Dragon LV3", "Twin-Headed Behemoth", "Harpie Lady 1" @@ -599,7 +599,7 @@ def only_fairy(state, player): return state.has_any([ "Dunames Dark Witch", "Hysteric Fairy" - ], player) and (state.count_from_list_exclusive([ + ], player) and (state.count_from_list_unique([ "Dunames Dark Witch", "Hysteric Fairy", "Dancing Fairy", @@ -623,7 +623,7 @@ def only_warrior(state, player): "Gearfried the Iron knight", "Ninja Grandmaster Sasuke", "Warrior Beaters" - ], player) and (state.count_from_list_exclusive([ + ], player) and (state.count_from_list_unique([ "Warrior Lady of the Wasteland", "Exiled Force", "Mystic Swordsman LV2", @@ -644,7 +644,7 @@ def only_warrior(state, player): def only_zombie(state, player): return state.has("Pyramid Turtle", player) \ - and state.has_from_list_exclusive([ + and state.has_from_list_unique([ "Regenerating Mummy", "Ryu Kokki", "Spirit Reaper", @@ -665,7 +665,7 @@ def only_dragon(state, player): "Luster Dragon", "Spear Dragon", "Cave Dragon" - ], player) and (state.count_from_list_exclusive([ + ], player) and (state.count_from_list_unique([ "Luster Dragon", "Spear Dragon", "Cave Dragon" @@ -692,7 +692,7 @@ def only_spellcaster(state, player): "Toon Gemini Elf", "Kycoo the Ghost Destroyer", "Familiar-Possessed - Aussa" - ], player) and (state.count_from_list_exclusive([ + ], player) and (state.count_from_list_unique([ "Dark Elf", "Gemini Elf", "Skilled Dark Magician", @@ -730,7 +730,7 @@ def equip_unions(state, player): def can_gain_lp_every_turn(state, player): - return state.count_from_list_exclusive([ + return state.count_from_list_unique([ "Solemn Wishes", "Cure Mermaid", "Dancing Fairy", @@ -739,7 +739,7 @@ def can_gain_lp_every_turn(state, player): def only_normal(state, player): - return (state.has_from_list_exclusive([ + return (state.has_from_list_unique([ "Archfiend Soldier", "Gemini Elf", "Insect Knight", @@ -784,21 +784,21 @@ def only_level(state, player): def spell_counter(state, player): return (state.has("Pitch-Black Power Stone", player) and - state.has_from_list_exclusive(["Blast Magician", - "Magical Marionette", - "Mythical Beast Cerberus", - "Royal Magical Library", - "Spell-Counter Cards"], player, 2)) + state.has_from_list_unique(["Blast Magician", + "Magical Marionette", + "Mythical Beast Cerberus", + "Royal Magical Library", + "Spell-Counter Cards"], player, 2)) def take_control(state, player): - return state.has_from_list_exclusive(["Aussa the Earth Charmer", - "Jowls of Dark Demise", - "Brain Control", - "Creature Swap", - "Enemy Controller", - "Mind Control", - "Magician of Faith"], player, 5) + return state.has_from_list_unique(["Aussa the Earth Charmer", + "Jowls of Dark Demise", + "Brain Control", + "Creature Swap", + "Enemy Controller", + "Mind Control", + "Magician of Faith"], player, 5) def only_toons(state, player): @@ -818,51 +818,51 @@ def only_spirit(state, player): def pacman_deck(state, player): - return state.has_from_list_exclusive(["Des Lacooda", - "Swarm of Locusts", - "Swarm of Scarabs", - "Wandering Mummy", - "Golem Sentry", - "Great Spirit", - "Royal Keeper", - "Stealth Bird"], player, 4) + return state.has_from_list_unique(["Des Lacooda", + "Swarm of Locusts", + "Swarm of Scarabs", + "Wandering Mummy", + "Golem Sentry", + "Great Spirit", + "Royal Keeper", + "Stealth Bird"], player, 4) def quick_plays(state, player): - return state.has_from_list_exclusive(["Collapse", - "Emergency Provisions", - "Enemy Controller", - "Graceful Dice", - "Mystik Wok", - "Offerings to the Doomed", - "Poison of the Old Man", - "Reload", - "Rush Recklessly", - "The Reliable Guardian"], player, 4) + return state.has_from_list_unique(["Collapse", + "Emergency Provisions", + "Enemy Controller", + "Graceful Dice", + "Mystik Wok", + "Offerings to the Doomed", + "Poison of the Old Man", + "Reload", + "Rush Recklessly", + "The Reliable Guardian"], player, 4) def counter_traps(state, player): - return state.has_from_list_exclusive(["Cursed Seal of the Forbidden Spell", - "Divine Wrath", - "Horn of Heaven", - "Magic Drain", - "Magic Jammer", - "Negate Attack", - "Seven Tools of the Bandit", - "Solemn Judgment", - "Spell Shield Type-8"], player, 5) + return state.has_from_list_unique(["Cursed Seal of the Forbidden Spell", + "Divine Wrath", + "Horn of Heaven", + "Magic Drain", + "Magic Jammer", + "Negate Attack", + "Seven Tools of the Bandit", + "Solemn Judgment", + "Spell Shield Type-8"], player, 5) def back_row_removal(state, player): - return state.has_from_list_exclusive(["Anteatereatingant", - "B.E.S. Tetran", - "Breaker the Magical Warrior", - "Calamity of the Wicked", - "Chiron the Mage", - "Dust Tornado", - "Heavy Storm", - "Mystical Space Typhoon", - "Mobius the Frost Monarch", - "Raigeki Break", - "Stamping Destruction", - "Swarm of Locusts"], player, 2) + return state.has_from_list_unique(["Anteatereatingant", + "B.E.S. Tetran", + "Breaker the Magical Warrior", + "Calamity of the Wicked", + "Chiron the Mage", + "Dust Tornado", + "Heavy Storm", + "Mystical Space Typhoon", + "Mobius the Frost Monarch", + "Raigeki Break", + "Stamping Destruction", + "Swarm of Locusts"], player, 2) From f249c36f8b86887e1ea3bbeb7013f9bd0723e3c1 Mon Sep 17 00:00:00 2001 From: black-sliver <59490463+black-sliver@users.noreply.github.com> Date: Sun, 26 May 2024 21:22:40 +0200 Subject: [PATCH 22/64] Setup: pin cx_freeze to 7.0.0 (#3406) 7.1.0 is broken on Linux when using pygobject, which we use as optional dependency for kivy. --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 3e128eec7e55..54d5118a2c50 100644 --- a/setup.py +++ b/setup.py @@ -21,7 +21,7 @@ # This is a bit jank. We need cx-Freeze to be able to run anything from this script, so install it try: - requirement = 'cx-Freeze>=7.0.0' + requirement = 'cx-Freeze==7.0.0' import pkg_resources try: pkg_resources.require(requirement) From 70d97a0eb43c424e000c86c9c3d1ddfa1611cee0 Mon Sep 17 00:00:00 2001 From: Bryce Wilson Date: Sun, 26 May 2024 17:27:04 -0700 Subject: [PATCH 23/64] BizHawkClient: Add suggestion when no handler is found (#3375) --- worlds/_bizhawk/context.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/worlds/_bizhawk/context.py b/worlds/_bizhawk/context.py index 0a28a47894d4..9fe6c9e1ffb1 100644 --- a/worlds/_bizhawk/context.py +++ b/worlds/_bizhawk/context.py @@ -177,7 +177,8 @@ async def _game_watcher(ctx: BizHawkClientContext): if ctx.client_handler is None: if not showed_no_handler_message: - logger.info("No handler was found for this game") + logger.info("No handler was found for this game. Double-check that the apworld is installed " + "correctly and that you loaded the right ROM file.") showed_no_handler_message = True continue else: From df877a9254784930801d13ed6531d50ff11c2fc8 Mon Sep 17 00:00:00 2001 From: Justus Lind Date: Mon, 27 May 2024 10:27:43 +1000 Subject: [PATCH 24/64] Muse Dash: 4.4.0 (#3395) --- worlds/musedash/MuseDashData.txt | 8 +++++++- worlds/musedash/Options.py | 2 +- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/worlds/musedash/MuseDashData.txt b/worlds/musedash/MuseDashData.txt index b0f3b80c997a..d822a3dc3839 100644 --- a/worlds/musedash/MuseDashData.txt +++ b/worlds/musedash/MuseDashData.txt @@ -519,7 +519,7 @@ Hey Vincent.|43-49|MD Plus Project|True|6|8|10| Meteor feat. TEA|43-50|MD Plus Project|True|3|6|9| Narcissism Angel|43-51|MD Plus Project|True|1|3|6| AlterLuna|43-52|MD Plus Project|True|6|8|11|12 -Niki Tousen|43-53|MD Plus Project|True|6|8|10|11 +Niki Tousen|43-53|MD Plus Project|True|6|8|10|12 Rettou Joutou|70-0|Rin Len's Mirrorland|False|4|7|9| Telecaster B-Boy|70-1|Rin Len's Mirrorland|False|5|7|10| Iya Iya Iya|70-2|Rin Len's Mirrorland|False|2|4|7| @@ -551,3 +551,9 @@ Dance Dance Good Night Dance|73-2|Happy Otaku Pack Vol.19|True|2|4|7| Ops Limone|73-3|Happy Otaku Pack Vol.19|True|5|8|11| NOVA|73-4|Happy Otaku Pack Vol.19|True|6|8|10| Heaven's Gradius|73-5|Happy Otaku Pack Vol.19|True|6|8|10| +Ray Tuning|74-0|CHUNITHM COURSE MUSE|True|6|8|10| +World Vanquisher|74-1|CHUNITHM COURSE MUSE|True|6|8|10|11 +Territory Battles|74-2|CHUNITHM COURSE MUSE|True|5|7|9| +The wheel to the right|74-3|CHUNITHM COURSE MUSE|True|5|7|9|11 +Climax|74-4|CHUNITHM COURSE MUSE|True|4|8|11|11 +Spider's Thread|74-5|CHUNITHM COURSE MUSE|True|5|8|10|12 diff --git a/worlds/musedash/Options.py b/worlds/musedash/Options.py index b695395135f6..4f4f52ad2d2d 100644 --- a/worlds/musedash/Options.py +++ b/worlds/musedash/Options.py @@ -38,7 +38,7 @@ class AdditionalSongs(Range): - The final song count may be lower due to other settings. """ range_start = 15 - range_end = 528 # Note will probably not reach this high if any other settings are done. + range_end = 534 # Note will probably not reach this high if any other settings are done. default = 40 display_name = "Additional Song Count" From 74aa4eca9dc7513b2a6579481576cceca3e797b1 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Mon, 27 May 2024 18:43:25 +0200 Subject: [PATCH 25/64] MultiServer: make !hint prefer early sphere (#2862) --- Main.py | 12 ++++++++++++ MultiServer.py | 22 +++++++++++++++++++++- 2 files changed, 33 insertions(+), 1 deletion(-) diff --git a/Main.py b/Main.py index 8b15a57a69e5..de6b467f93d9 100644 --- a/Main.py +++ b/Main.py @@ -372,6 +372,17 @@ def precollect_hint(location): checks_in_area: Dict[int, Dict[str, Union[int, List[int]]]] = {} + # get spheres -> filter address==None -> skip empty + spheres: List[Dict[int, Set[int]]] = [] + for sphere in multiworld.get_spheres(): + current_sphere: Dict[int, Set[int]] = collections.defaultdict(set) + for sphere_location in sphere: + if type(sphere_location.address) is int: + current_sphere[sphere_location.player].add(sphere_location.address) + + if current_sphere: + spheres.append(dict(current_sphere)) + multidata = { "slot_data": slot_data, "slot_info": slot_info, @@ -386,6 +397,7 @@ def precollect_hint(location): "tags": ["AP"], "minimum_versions": minimum_versions, "seed_name": multiworld.seed_name, + "spheres": spheres, "datapackage": data_package, } AutoWorld.call_all(multiworld, "modify_multidata", multidata) diff --git a/MultiServer.py b/MultiServer.py index e95e44dd7d5c..4fb03732d811 100644 --- a/MultiServer.py +++ b/MultiServer.py @@ -175,8 +175,11 @@ class Context: all_item_and_group_names: typing.Dict[str, typing.Set[str]] all_location_and_group_names: typing.Dict[str, typing.Set[str]] non_hintable_names: typing.Dict[str, typing.Set[str]] + spheres: typing.List[typing.Dict[int, typing.Set[int]]] + """ each sphere is { player: { location_id, ... } } """ logger: logging.Logger + def __init__(self, host: str, port: int, server_password: str, password: str, location_check_points: int, hint_cost: int, item_cheat: bool, release_mode: str = "disabled", collect_mode="disabled", remaining_mode: str = "disabled", auto_shutdown: typing.SupportsFloat = 0, compatibility: int = 2, @@ -238,6 +241,7 @@ def __init__(self, host: str, port: int, server_password: str, password: str, lo self.stored_data = {} self.stored_data_notification_clients = collections.defaultdict(weakref.WeakSet) self.read_data = {} + self.spheres = [] # init empty to satisfy linter, I suppose self.gamespackage = {} @@ -466,6 +470,9 @@ def _load(self, decoded_obj: dict, game_data_packages: typing.Dict[str, typing.A for game_name, data in self.location_name_groups.items(): self.read_data[f"location_name_groups_{game_name}"] = lambda lgame=game_name: self.location_name_groups[lgame] + # sorted access spheres + self.spheres = decoded_obj.get("spheres", []) + # saving def save(self, now=False) -> bool: @@ -624,6 +631,16 @@ def get_rechecked_hints(self, team: int, slot: int): self.recheck_hints(team, slot) return self.hints[team, slot] + def get_sphere(self, player: int, location_id: int) -> int: + """Get sphere of a location, -1 if spheres are not available.""" + if self.spheres: + for i, sphere in enumerate(self.spheres): + if location_id in sphere.get(player, set()): + return i + raise KeyError(f"No Sphere found for location ID {location_id} belonging to player {player}. " + f"Location or player may not exist.") + return -1 + def get_players_package(self): return [NetworkPlayer(t, p, self.get_aliased_name(t, p), n) for (t, p), n in self.player_names.items()] @@ -1549,6 +1566,9 @@ def get_hints(self, input_text: str, for_location: bool = False) -> bool: self.ctx.random.shuffle(not_found_hints) # By popular vote, make hints prefer non-local placements not_found_hints.sort(key=lambda hint: int(hint.receiving_player != hint.finding_player)) + # By another popular vote, prefer early sphere + not_found_hints.sort(key=lambda hint: self.ctx.get_sphere(hint.finding_player, hint.location), + reverse=True) hints = found_hints + old_hints while can_pay > 0: @@ -1558,10 +1578,10 @@ def get_hints(self, input_text: str, for_location: bool = False) -> bool: hints.append(hint) can_pay -= 1 self.ctx.hints_used[self.client.team, self.client.slot] += 1 - points_available = get_client_points(self.ctx, self.client) self.ctx.notify_hints(self.client.team, hints) if not_found_hints: + points_available = get_client_points(self.ctx, self.client) if hints and cost and int((points_available // cost) == 0): self.output( f"There may be more hintables, however, you cannot afford to pay for any more. " From dfc347cd241494ee79af90cfedba9265d1122dcf Mon Sep 17 00:00:00 2001 From: Aaron Wagener Date: Mon, 27 May 2024 16:52:23 -0500 Subject: [PATCH 26/64] Core: add options to the list of valid names instead of deleting game weights (#3381) --- Generate.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Generate.py b/Generate.py index 30b992317d73..fab34c893ae9 100644 --- a/Generate.py +++ b/Generate.py @@ -432,7 +432,6 @@ def handle_option(ret: argparse.Namespace, game_weights: dict, option_key: str, player_option = option.from_any(game_weights[option_key]) else: player_option = option.from_any(get_choice(option_key, game_weights)) - del game_weights[option_key] else: player_option = option.from_any(option.default) # call the from_any here to support default "random" setattr(ret, option_key, player_option) @@ -446,9 +445,9 @@ def roll_settings(weights: dict, plando_options: PlandoOptions = PlandoOptions.b if "linked_options" in weights: weights = roll_linked_options(weights) - valid_trigger_names = set() + valid_keys = set() if "triggers" in weights: - weights = roll_triggers(weights, weights["triggers"], valid_trigger_names) + weights = roll_triggers(weights, weights["triggers"], valid_keys) requirements = weights.get("requires", {}) if requirements: @@ -490,7 +489,7 @@ def roll_settings(weights: dict, plando_options: PlandoOptions = PlandoOptions.b raise Exception(f"Remove tag cannot be used outside of trigger contexts. Found {weight}") if "triggers" in game_weights: - weights = roll_triggers(weights, game_weights["triggers"], valid_trigger_names) + weights = roll_triggers(weights, game_weights["triggers"], valid_keys) game_weights = weights[ret.game] ret.name = get_choice('name', weights) @@ -499,8 +498,9 @@ def roll_settings(weights: dict, plando_options: PlandoOptions = PlandoOptions.b for option_key, option in world_type.options_dataclass.type_hints.items(): handle_option(ret, game_weights, option_key, option, plando_options) + valid_keys.add(option_key) for option_key in game_weights: - if option_key in {"triggers", *valid_trigger_names}: + if option_key in {"triggers", *valid_keys}: continue logging.warning(f"{option_key} is not a valid option name for {ret.game} and is not present in triggers.") if PlandoOptions.items in plando_options: From 04e9f5c47ab55305d6d0babc7d53283e1bc62b32 Mon Sep 17 00:00:00 2001 From: Seldom <38388947+Seldom-SE@users.noreply.github.com> Date: Tue, 28 May 2024 11:37:07 -0700 Subject: [PATCH 27/64] Migrate Terraria to new options API (#3414) --- worlds/terraria/Options.py | 16 ++++++++-------- worlds/terraria/__init__.py | 19 ++++++++++--------- 2 files changed, 18 insertions(+), 17 deletions(-) diff --git a/worlds/terraria/Options.py b/worlds/terraria/Options.py index 1f9ba69afeb9..4c4b96056c5f 100644 --- a/worlds/terraria/Options.py +++ b/worlds/terraria/Options.py @@ -1,5 +1,5 @@ -from Options import Choice, Option, Toggle, DeathLink -import typing +from dataclasses import dataclass +from Options import Choice, DeathLink, PerGameCommonOptions class Goal(Choice): @@ -49,9 +49,9 @@ class FillExtraChecksWith(Choice): default = 1 -options: typing.Dict[str, type(Option)] = { # type: ignore - "goal": Goal, - "achievements": Achievements, - "fill_extra_checks_with": FillExtraChecksWith, - "death_link": DeathLink, -} +@dataclass +class TerrariaOptions(PerGameCommonOptions): + goal: Goal + achievements: Achievements + fill_extra_checks_with: FillExtraChecksWith + death_link: DeathLink diff --git a/worlds/terraria/__init__.py b/worlds/terraria/__init__.py index 6ef281157f9d..ac6b25e51632 100644 --- a/worlds/terraria/__init__.py +++ b/worlds/terraria/__init__.py @@ -25,7 +25,7 @@ armor_minions, accessory_minions, ) -from .Options import options +from .Options import TerrariaOptions class TerrariaWeb(WebWorld): @@ -49,7 +49,8 @@ class TerrariaWorld(World): game = "Terraria" web = TerrariaWeb() - option_definitions = options + options_dataclass = TerrariaOptions + options: TerrariaOptions # data_version is used to signal that items, locations or their names # changed. Set this to 0 during development so other games' clients do not @@ -70,7 +71,7 @@ class TerrariaWorld(World): goal_locations: Set[str] def generate_early(self) -> None: - goal, goal_locations = goals[self.multiworld.goal[self.player].value] + goal, goal_locations = goals[self.options.goal.value] ter_goals = {} goal_items = set() for location in goal_locations: @@ -79,7 +80,7 @@ def generate_early(self) -> None: ter_goals[item] = location goal_items.add(item) - achievements = self.multiworld.achievements[self.player].value + achievements = self.options.achievements.value location_count = 0 locations = [] for rule, flags, _, _ in rules[:goal]: @@ -89,7 +90,7 @@ def generate_early(self) -> None: or (achievements < 2 and "Grindy" in flags) or (achievements < 3 and "Fishing" in flags) or ( - rule == "Zenith" and self.multiworld.goal[self.player].value != 11 + rule == "Zenith" and self.options.goal.value != 11 ) # Bad hardcoding ): continue @@ -123,7 +124,7 @@ def generate_early(self) -> None: # Event items.append(rule) - extra_checks = self.multiworld.fill_extra_checks_with[self.player].value + extra_checks = self.options.fill_extra_checks_with.value ordered_rewards = [ reward for reward in labels["ordered"] @@ -241,7 +242,7 @@ def check_condition( elif condition == "calamity": return sign == self.calamity elif condition == "grindy": - return sign == (self.multiworld.achievements[self.player].value >= 2) + return sign == (self.options.achievements.value >= 2) elif condition == "pickaxe": if type(arg) is not int: raise Exception("@pickaxe requires an integer argument") @@ -340,6 +341,6 @@ def check(state: CollectionState, location=location): def fill_slot_data(self) -> Dict[str, object]: return { "goal": list(self.goal_locations), - "achievements": self.multiworld.achievements[self.player].value, - "deathlink": bool(self.multiworld.death_link[self.player]), + "achievements": self.options.achievements.value, + "deathlink": bool(self.options.death_link), } From 5b34e06c8ba39cd04977e4022871cc5699303561 Mon Sep 17 00:00:00 2001 From: qwint Date: Tue, 28 May 2024 20:37:44 -0500 Subject: [PATCH 28/64] adds godtuner to prog and requires it for godhome flower quest manually (#3402) --- worlds/hk/GodhomeData.py | 2 +- worlds/hk/__init__.py | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/worlds/hk/GodhomeData.py b/worlds/hk/GodhomeData.py index 6e9d77f4dc47..a2dd69ed73ef 100644 --- a/worlds/hk/GodhomeData.py +++ b/worlds/hk/GodhomeData.py @@ -9,7 +9,7 @@ def set_godhome_rules(hk_world, hk_set_rule): fn = partial(hk_set_rule, hk_world) required_events = { - "Godhome_Flower_Quest": lambda state: state.count('Defeated_Pantheon_5', player) and state.count('Room_Mansion[left1]', player) and state.count('Fungus3_49[right1]', player), + "Godhome_Flower_Quest": lambda state: state.count('Defeated_Pantheon_5', player) and state.count('Room_Mansion[left1]', player) and state.count('Fungus3_49[right1]', player) and state.has('Godtuner', player), "Defeated_Pantheon_5": lambda state: state.has('GG_Atrium_Roof', player) and state.has('WINGS', player) and (state.has('LEFTCLAW', player) or state.has('RIGHTCLAW', player)) and ((state.has('Defeated_Pantheon_1', player) and state.has('Defeated_Pantheon_2', player) and state.has('Defeated_Pantheon_3', player) and state.has('Defeated_Pantheon_4', player) and state.has('COMBAT[Radiance]', player))), "GG_Atrium_Roof": lambda state: state.has('GG_Atrium', player) and state.has('Hit_Pantheon_5_Unlock_Orb', player) and state.has('LEFTCLAW', player), diff --git a/worlds/hk/__init__.py b/worlds/hk/__init__.py index 1359bea5ce6d..3530030fa695 100644 --- a/worlds/hk/__init__.py +++ b/worlds/hk/__init__.py @@ -659,6 +659,8 @@ class HKItem(Item): def __init__(self, name, advancement, code, type: str, player: int = None): if name == "Mimic_Grub": classification = ItemClassification.trap + elif name == "Godtuner": + classification = ItemClassification.progression elif type in ("Grub", "DreamWarrior", "Root", "Egg", "Dreamer"): classification = ItemClassification.progression_skip_balancing elif type == "Charm" and name not in progression_charms: From 649ee117dafbd49511899a7bf4c100815688dc1c Mon Sep 17 00:00:00 2001 From: Aaron Wagener Date: Tue, 28 May 2024 20:46:17 -0500 Subject: [PATCH 29/64] Docs: improve contributing sign posting (#2888) * Docs: improve sign posting for contributing * fix styling as per the style guide * address review comments * apply medic's feedback --- README.md | 45 ++++++++++++++++++++++++++--------- docs/contributing.md | 56 ++++++++++++++++++++++++-------------------- 2 files changed, 65 insertions(+), 36 deletions(-) diff --git a/README.md b/README.md index 4633c99c664d..cebd4f7e7529 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,10 @@ # [Archipelago](https://archipelago.gg) ![Discord Shield](https://discordapp.com/api/guilds/731205301247803413/widget.png?style=shield) | [Install](https://github.com/ArchipelagoMW/Archipelago/releases) -Archipelago provides a generic framework for developing multiworld capability for game randomizers. In all cases, presently, Archipelago is also the randomizer itself. +Archipelago provides a generic framework for developing multiworld capability for game randomizers. In all cases, +presently, Archipelago is also the randomizer itself. Currently, the following games are supported: + * The Legend of Zelda: A Link to the Past * Factorio * Minecraft @@ -77,36 +79,57 @@ windows binaries. ## History -Archipelago is built upon a strong legacy of brilliant hobbyists. We want to honor that legacy by showing it here. The repositories which Archipelago is built upon, inspired by, or otherwise owes its gratitude to are: +Archipelago is built upon a strong legacy of brilliant hobbyists. We want to honor that legacy by showing it here. +The repositories which Archipelago is built upon, inspired by, or otherwise owes its gratitude to are: * [bonta0's MultiWorld](https://github.com/Bonta0/ALttPEntranceRandomizer/tree/multiworld_31) * [AmazingAmpharos' Entrance Randomizer](https://github.com/AmazingAmpharos/ALttPEntranceRandomizer) * [VT Web Randomizer](https://github.com/sporchia/alttp_vt_randomizer) * [Dessyreqt's alttprandomizer](https://github.com/Dessyreqt/alttprandomizer) -* [Zarby89's](https://github.com/Ijwu/Enemizer/commits?author=Zarby89) and [sosuke3's](https://github.com/Ijwu/Enemizer/commits?author=sosuke3) contributions to Enemizer, which make the vast majority of Enemizer contributions. +* [Zarby89's](https://github.com/Ijwu/Enemizer/commits?author=Zarby89) + and [sosuke3's](https://github.com/Ijwu/Enemizer/commits?author=sosuke3) contributions to Enemizer, which make up the + vast majority of Enemizer contributions. -We recognize that there is a strong community of incredibly smart people that have come before us and helped pave the path. Just because one person's name may be in a repository title does not mean that only one person made that project happen. We can't hope to perfectly cover every single contribution that lead up to Archipelago but we hope to honor them fairly. +We recognize that there is a strong community of incredibly smart people that have come before us and helped pave the +path. Just because one person's name may be in a repository title does not mean that only one person made that project +happen. We can't hope to perfectly cover every single contribution that lead up to Archipelago, but we hope to honor +them fairly. ### Path to the Archipelago -Archipelago was directly forked from bonta0's `multiworld_31` branch of ALttPEntranceRandomizer (this project has a long legacy of its own, please check it out linked above) on January 12, 2020. The repository was then named to _MultiWorld-Utilities_ to better encompass its intended function. As Archipelago matured, then known as "Berserker's MultiWorld" by some, we found it necessary to transform our repository into a root level repository (as opposed to a 'forked repo') and change the name (which came later) to better reflect our project. + +Archipelago was directly forked from bonta0's `multiworld_31` branch of ALttPEntranceRandomizer (this project has a +long legacy of its own, please check it out linked above) on January 12, 2020. The repository was then named to +_MultiWorld-Utilities_ to better encompass its intended function. As Archipelago matured, then known as +"Berserker's MultiWorld" by some, we found it necessary to transform our repository into a root level repository +(as opposed to a 'forked repo') and change the name (which came later) to better reflect our project. ## Running Archipelago -For most people, all you need to do is head over to the [releases](https://github.com/ArchipelagoMW/Archipelago/releases) page then download and run the appropriate installer, or AppImage for Linux-based systems. -If you are a developer or are running on a platform with no compiled releases available, please see our doc on [running Archipelago from source](docs/running%20from%20source.md). +For most people, all you need to do is head over to +the [releases page](https://github.com/ArchipelagoMW/Archipelago/releases), then download and run the appropriate +installer, or AppImage for Linux-based systems. + +If you are a developer or are running on a platform with no compiled releases available, please see our doc on +[running Archipelago from source](docs/running%20from%20source.md). ## Related Repositories -This project makes use of multiple other projects. We wouldn't be here without these other repositories and the contributions of their developers, past and present. + +This project makes use of multiple other projects. We wouldn't be here without these other repositories and the +contributions of their developers, past and present. * [z3randomizer](https://github.com/ArchipelagoMW/z3randomizer) * [Enemizer](https://github.com/Ijwu/Enemizer) * [Ocarina of Time Randomizer](https://github.com/TestRunnerSRL/OoT-Randomizer) ## Contributing -For contribution guidelines, please see our [Contributing doc.](/docs/contributing.md) + +To contribute to Archipelago, including the WebHost, core program, or by adding a new game, see our +[Contributing guidelines](/docs/contributing.md). ## FAQ -For Frequently asked questions, please see the website's [FAQ Page.](https://archipelago.gg/faq/en/) + +For Frequently asked questions, please see the website's [FAQ Page](https://archipelago.gg/faq/en/). ## Code of Conduct -Please refer to our [code of conduct.](/docs/code_of_conduct.md) + +Please refer to our [code of conduct](/docs/code_of_conduct.md). diff --git a/docs/contributing.md b/docs/contributing.md index e7f3516712d2..9fd21408eb7b 100644 --- a/docs/contributing.md +++ b/docs/contributing.md @@ -1,43 +1,49 @@ # Contributing -Contributions are welcome. We have a few requests for new contributors: + +All contributions are welcome, though we have a few requests of contributors, whether they be for core, webhost, or new +game contributions: * **Follow styling guidelines.** Please take a look at the [code style documentation](/docs/style.md) to ensure ease of communication and uniformity. -* **Ensure that critical changes are covered by tests.** -It is strongly recommended that unit tests are used to avoid regression and to ensure everything is still working. -If you wish to contribute by adding a new game, please take a look at the [logic unit test documentation](/docs/tests.md). -If you wish to contribute to the website, please take a look at [these tests](/test/webhost). +* **Ensure that critical changes are covered by tests.** + It is strongly recommended that unit tests are used to avoid regression and to ensure everything is still working. + If you wish to contribute by adding a new game, please take a look at + the [logic unit test documentation](/docs/tests.md). + If you wish to contribute to the website, please take a look at [these tests](/test/webhost). * **Do not introduce unit test failures/regressions.** -Archipelago supports multiple versions of Python. You may need to download older Python versions to fully test -your changes. Currently, the oldest supported version is [Python 3.8](https://www.python.org/downloads/release/python-380/). -It is recommended that automated github actions are turned on in your fork to have github run all of the unit tests after pushing. -You can turn them on here: -![Github actions example](./img/github-actions-example.png) + Archipelago supports multiple versions of Python. You may need to download older Python versions to fully test + your changes. Currently, the oldest supported version + is [Python 3.8](https://www.python.org/downloads/release/python-380/). + It is recommended that automated github actions are turned on in your fork to have github run unit tests after + pushing. + You can turn them on here: + ![Github actions example](./img/github-actions-example.png) * **When reviewing PRs, please leave a message about what was done.** -We don't have full test coverage, so manual testing can help. -For code changes that could affect multiple worlds or that could have changes in unexpected code paths, manual testing -or checking if all code paths are covered by automated tests is desired. The original author may not have been able -to test all possibly affected worlds, or didn't know it would affect another world. In such cases, it is helpful to -state which games or settings were rolled, if any. -Please also tell us if you looked at code, just did functional testing, did both, or did neither. -If testing the PR depends on other PRs, please state what you merged into what for testing. -We cannot determine what "LGTM" means without additional context, so that should not be the norm. + We don't have full test coverage, so manual testing can help. + For code changes that could affect multiple worlds or that could have changes in unexpected code paths, manual testing + or checking if all code paths are covered by automated tests is desired. The original author may not have been able + to test all possibly affected worlds, or didn't know it would affect another world. In such cases, it is helpful to + state which games or settings were rolled, if any. + Please also tell us if you looked at code, just did functional testing, did both, or did neither. + If testing the PR depends on other PRs, please state what you merged into what for testing. + We cannot determine what "LGTM" means without additional context, so that should not be the norm. -Other than these requests, we tend to judge code on a case-by-case basis. +Other than these requests, we tend to judge code on a case-by-case basis. For contribution to the website, please refer to the [WebHost README](/WebHostLib/README.md). If you want to contribute to the core, you will be subject to stricter review on your pull requests. It is recommended that you get in touch with other core maintainers via the [Discord](https://archipelago.gg/discord). -If you want to add Archipelago support for a new game, please take a look at the [adding games documentation](/docs/adding%20games.md), which details what is required -to implement support for a game, as well as tips for how to get started. -If you want to merge a new game into the main Archipelago repo, please make sure to read the responsibilities as a -[world maintainer](/docs/world%20maintainer.md). +If you want to add Archipelago support for a new game, please take a look at +the [adding games documentation](/docs/adding%20games.md) +which details what is required to implement support for a game, and has tips on to get started. +If you want to merge a new game into the main Archipelago repo, please make sure to read the responsibilities as a +[world maintainer](/docs/world%20maintainer.md). -For other questions, feel free to explore the [main documentation folder](/docs/) and ask us questions in the #archipelago-dev channel -of the [Discord](https://archipelago.gg/discord). +For other questions, feel free to explore the [main documentation folder](/docs), and ask us questions in the +#ap-world-dev channel of the [Discord](https://archipelago.gg/discord). From 527559395c2d7bb7e4f05835b0ec6548a803caa6 Mon Sep 17 00:00:00 2001 From: neocerber <140952826+neocerber@users.noreply.github.com> Date: Tue, 28 May 2024 18:48:52 -0700 Subject: [PATCH 30/64] Docs, Starcraft 2: Add French documentation for setup and game page (#3031) * Started to create the french doc * First version of sc2 setup in french finish, created the file for the introduction of the game in french * French-fy upgrade in setup, continue translation of game description * Finish writing FR game page, added a link to it on the english game page. Re-read and corrected both the game page and setup page. * Corrected a sentence in the SC2 English setup guide. * Applied 120 carac limits for french part, applied modification for consistency. * Added reference to website yaml checker, applied several wording correction/suggestions * Modified link to AP page to be in relative (fr/en), uniformed SC2 and random writing (fr), applied some suggestons in writing quality(fr), added a mention to the datapackage (fr/en), enhanced prog balancing recommendation (fr) * Correction of some grammar issues * Removed name correction for english part since done in other PR; added mention to hotkey and language restriction * Applied suggestions of peer review * Applied mofications proposed by reviewer about the external website --------- Co-authored-by: neocerber --- worlds/sc2/__init__.py | 13 +- worlds/sc2/docs/en_Starcraft 2.md | 5 +- worlds/sc2/docs/fr_Starcraft 2.md | 95 +++++++++++++ worlds/sc2/docs/setup_en.md | 22 ++- worlds/sc2/docs/setup_fr.md | 214 ++++++++++++++++++++++++++++++ 5 files changed, 339 insertions(+), 10 deletions(-) create mode 100644 worlds/sc2/docs/fr_Starcraft 2.md create mode 100644 worlds/sc2/docs/setup_fr.md diff --git a/worlds/sc2/__init__.py b/worlds/sc2/__init__.py index 59c6fe900197..ec8a447d931e 100644 --- a/worlds/sc2/__init__.py +++ b/worlds/sc2/__init__.py @@ -22,7 +22,7 @@ class Starcraft2WebWorld(WebWorld): - setup = Tutorial( + setup_en = Tutorial( "Multiworld Setup Guide", "A guide to setting up the Starcraft 2 randomizer connected to an Archipelago Multiworld", "English", @@ -31,7 +31,16 @@ class Starcraft2WebWorld(WebWorld): ["TheCondor", "Phaneros"] ) - tutorials = [setup] + setup_fr = Tutorial( + setup_en.tutorial_name, + setup_en.description, + "Français", + "setup_fr.md", + "setup/fr", + ["Neocerber"] + ) + + tutorials = [setup_en, setup_fr] class SC2World(World): diff --git a/worlds/sc2/docs/en_Starcraft 2.md b/worlds/sc2/docs/en_Starcraft 2.md index 784d711319d8..06464e3cd2fd 100644 --- a/worlds/sc2/docs/en_Starcraft 2.md +++ b/worlds/sc2/docs/en_Starcraft 2.md @@ -1,5 +1,8 @@ # Starcraft 2 +## Game page in other languages: +* [Français](/games/Starcraft%202/info/fr) + ## What does randomization do to this game? The following unlocks are randomized as items: @@ -39,7 +42,7 @@ The goal is to beat the final mission in the mission order. The yaml configurati ## Which of my items can be in another player's world? By default, any of StarCraft 2's items (specified above) can be in another player's world. See the -[Advanced YAML Guide](https://archipelago.gg/tutorial/Archipelago/advanced_settings/en) +[Advanced YAML Guide](/tutorial/Archipelago/advanced_settings/en) for more information on how to change this. ## Unique Local Commands diff --git a/worlds/sc2/docs/fr_Starcraft 2.md b/worlds/sc2/docs/fr_Starcraft 2.md new file mode 100644 index 000000000000..4fcc8e689baa --- /dev/null +++ b/worlds/sc2/docs/fr_Starcraft 2.md @@ -0,0 +1,95 @@ +# *StarCraft 2* + +## Quel est l'effet de la *randomization* sur ce jeu ? + +Les éléments qui suivent sont les *items* qui sont *randomized* et qui doivent être débloqués pour être utilisés dans +le jeu: +1. La capacité de produire des unités, excepté les drones/probes/scv. +2. Des améliorations spécifiques à certaines unités incluant quelques combinaisons qui ne sont pas disponibles dans les +campagnes génériques, comme le fait d'avoir les deux types d'évolution en même temps pour une unité *Zerg* et toutes +les améliorations de la *Spear of Adun* simultanément pour les *Protoss*. +3. L'accès aux améliorations génériques des unités, e.g. les améliorations d'attaque et d'armure. +4. D'autres améliorations diverses telles que les améliorations de laboratoire et les mercenaires pour les *Terran*, +les niveaux et les améliorations de Kerrigan pour les *Zerg*, et les améliorations de la *Spear of Adun* pour les +*Protoss*. +5. Avoir des *minerals*, du *vespene gas*, et du *supply* au début de chaque mission. + +Les *items* sont trouvés en accomplissant du progrès dans les catégories suivantes: +* Terminer des missions +* Réussir des objectifs supplémentaires (e.g., récolter le matériel pour les recherches dans *Wings of Liberty*) +* Atteindre des étapes importantes dans la mission, e.g. réussir des sous-objectifs +* Réussir des défis basés sur les succès du jeu de base, e.g. éliminer tous les *Zerg* dans la mission +*Devil's Playground* + +Ces catégories, outre la première, peuvent être désactivées dans les options du jeu. +Par exemple, vous pouvez désactiver le fait d'obtenir des *items* lorsque des étapes importantes d'une mission sont +accomplies. + +Quand vous recevez un *item*, il devient immédiatement disponible, même pendant une mission, et vous serez avertis via +la boîte de texte situé dans le coin en haut à droite de *StarCraft 2*. +L'acquisition d'un *item* est aussi indiquée dans le client d'Archipelago. + +Les missions peuvent être lancées par le client *StarCraft 2 Archipelago*, via l'interface graphique de l'onglet +*StarCraft 2 Launcher*. +Les segments qui se passent sur l'*Hyperion*, un Léviathan et la *Spear of Adun* ne sont pas inclus. +De plus, les points de progression tels que les crédits ou la Solarite ne sont pas utilisés dans *StarCraft 2 +Archipelago*. + +## Quel est le but de ce jeu quand il est *randomized*? + +Le but est de réussir la mission finale dans la disposition des missions (e.g. *blitz*, *grid*, etc.). +Les choix faits dans le fichier *yaml* définissent la disposition des missions et comment elles sont mélangées. + +## Quelles sont les modifications non aléatoires comparativement à la version de base de *StarCraft 2* + +1. Certaines des missions ont plus de *vespene geysers* pour permettre l'utilisation d'une plus grande variété d'unités. +2. Plusieurs unités et améliorations ont été ajoutées sous la forme d*items*. +Ils proviennent de la version *co-op*, *melee*, des autres campagnes, d'expansions ultérieures, de *Brood War*, ou de +l'imagination des développeurs de *StarCraft 2 Archipelago*. +3. Les structures de production, e.g. *Factory*, *Starport*, *Robotics Facility*, and *Stargate*, n'ont plus +d'exigences technologiques. +4. Les missions avec la race *Zerg* ont été modifiées pour que les joueurs débuttent avec un *Lair* lorsqu'elles +commençaient avec une *Hatchery*. +5. Les désavantages des améliorations ont été enlevés, e.g. *automated refinery* qui coûte plus cher ou les *tech +reactors* qui prennent plus de temps à construire. +6. La collision des unités dans les couloirs de la mission *Enemy Within* a été ajustée pour permettre des unités +plus larges de les traverser sans être coincés dans des endroits étranges. +7. Plusieurs *bugs* du jeu original ont été corrigés. + +## Quels sont les *items* qui peuvent être dans le monde d'un autre joueur? + +Par défaut, tous les *items* de *StarCraft 2 Archipelago* (voir la section précédente) peuvent être dans le monde d'un +autre joueur. +Consulter [*Advanced YAML Guide*](/tutorial/Archipelago/advanced_settings/en) pour savoir comment +changer ça. + +## Commandes du client qui sont uniques à ce jeu + +Les commandes qui suivent sont seulement disponibles uniquement pour le client de *StarCraft 2 Archipelago*. +Vous pouvez les afficher en utilisant la commande `/help` dans le client de *StarCraft 2 Archipelago*. +Toutes ces commandes affectent seulement le client où elles sont utilisées. + +* `/download_data` Télécharge les versions les plus récentes des fichiers pour jouer à *StarCraft 2 Archipelago*. +Les fichiers existants vont être écrasés. +* `/difficulty [difficulty]` Remplace la difficulté choisie pour le monde. + * Les options sont *casual*, *normal*, *hard*, et *brutal*. +* `/game_speed [game_speed]` Remplace la vitesse du jeu pour le monde. + * Les options sont *default*, *slower*, *slow*, *normal*, *fast*, and *faster*. +* `/color [faction] [color]` Remplace la couleur d'une des *factions* qui est jouable. + * Les options de *faction*: raynor, kerrigan, primal, protoss, nova. + * Les options de couleur: *white*, *red*, *blue*, *teal*, *purple*, *yellow*, *orange*, *green*, *lightpink*, +*violet*, *lightgrey*, *darkgreen*, *brown*, *lightgreen*, *darkgrey*, *pink*, *rainbow*, *random*, *default*. +* `/option [option_name] [option_value]` Permet de changer un option normalement définit dans le *yaml*. + * Si la commande est lancée sans option, la liste des options qui sont modifiables va être affichée. + * Les options qui peuvent être changées avec cette commande incluent sauter les cinématiques automatiquement, la +présence de Kerrigan dans les missions, la disponibilité de la *Spear of Adun*, la quantité de ressources +supplémentaires données au début des missions, la capacité de contrôler les alliées IA, etc. +* `/disable_mission_check` Désactive les requit pour lancer les missions. +Cette option a pour but de permettre de jouer en mode coopératif en permettant à un joueur de jouer à la prochaine +mission de la chaîne qu'un autre joueur est en train d'entamer. +* `/play [mission_id]` Lance la mission correspondant à l'identifiant donné. +* `/available` Affiche les missions qui sont présentement accessibles. +* `/unfinished` Affiche les missions qui sont présentement accessibles et dont certains des objectifs permettant +l'accès à un *item* n'ont pas été accomplis. +* `/set_path [path]` Permet de définir manuellement où *StarCraft 2* est installé ce qui est pertinent seulement si la +détection automatique de cette dernière échoue. diff --git a/worlds/sc2/docs/setup_en.md b/worlds/sc2/docs/setup_en.md index 4956109778ff..991ed57e8741 100644 --- a/worlds/sc2/docs/setup_en.md +++ b/worlds/sc2/docs/setup_en.md @@ -23,18 +23,20 @@ Yaml files are configuration files that tell Archipelago how you'd like your gam When you're setting up a multiworld, every world needs its own yaml file. There are three basic ways to get a yaml: -* You can go to the [Player Options](https://archipelago.gg/games/Starcraft%202/player-options) page, set your options in the GUI, and export the yaml. -* You can generate a template, either by downloading it from the [Player Options](https://archipelago.gg/games/Starcraft%202/player-options) page or by generating it from the Launcher (ArchipelagoLauncher.exe). The template includes descriptions of each option, you just have to edit it in your text editor of choice. +* You can go to the [Player Options](/games/Starcraft%202/player-options) page, set your options in the GUI, and export the yaml. +* You can generate a template, either by downloading it from the [Player Options](/games/Starcraft%202/player-options) page or by generating it from the Launcher (ArchipelagoLauncher.exe). The template includes descriptions of each option, you just have to edit it in your text editor of choice. * You can ask someone else to share their yaml to use it for yourself or adjust it as you wish. Remember the name you enter in the options page or in the yaml file, you'll need it to connect later! -Check out [Creating a YAML](https://archipelago.gg/tutorial/Archipelago/setup/en#creating-a-yaml) for more game-agnostic information. +Check out [Creating a YAML](/tutorial/Archipelago/setup/en#creating-a-yaml) for more game-agnostic information. ### Common yaml questions #### How do I know I set my yaml up correctly? -The simplest way to check is to test it out. Save your yaml to the Players/ folder within your Archipelago installation and run ArchipelagoGenerate.exe. You should see a new .zip file within the output/ folder of your Archipelago installation if things worked correctly. It's advisable to run ArchipelagoGenerate through a terminal so that you can see the printout, which will include any errors and the precise output file name if it's successful. If you don't like terminals, you can also check the log file in the logs/ folder. +The simplest way to check is to use the website [validator](/check). + +You can also test it by attempting to generate a multiworld with your yaml. Save your yaml to the Players/ folder within your Archipelago installation and run ArchipelagoGenerate.exe. You should see a new .zip file within the output/ folder of your Archipelago installation if things worked correctly. It's advisable to run ArchipelagoGenerate through a terminal so that you can see the printout, which will include any errors and the precise output file name if it's successful. If you don't like terminals, you can also check the log file in the logs/ folder. #### What does Progression Balancing do? @@ -64,9 +66,15 @@ start_inventory: An empty mapping is just a matching pair of curly braces: `{}`. That's the default value in the template, which should let you know to use this syntax. -#### How do I know the exact names of items? +#### How do I know the exact names of items and locations? + +The [*datapackage*](/datapackage) page of the Archipelago website provides a complete list of the items and locations for each game that it currently supports, including StarCraft 2. + +You can also look up a complete list of the item names in the [Icon Repository](https://matthewmarinets.github.io/ap_sc2_icons/) page. +This page also contains supplementary information of each item. +However, the items shown in that page might differ from those shown in the datapackage page of Archipelago since the former is generated, most of the time, from beta versions of StarCraft 2 Archipelago undergoing development. -You can look up a complete list if item names in the [Icon Repository](https://matthewmarinets.github.io/ap_sc2_icons/). +As for the locations, you can see all the locations associated to a mission in your world by placing your cursor over the mission in the 'StarCraft 2 Launcher' tab in the client. ## How do I join a MultiWorld game? @@ -86,7 +94,7 @@ specific description of what's going wrong and attach your log file to your mess ## Running in macOS -To run StarCraft 2 through Archipelago in macOS, you will need to run the client via source as seen here: [macOS Guide](https://archipelago.gg/tutorial/Archipelago/mac/en). Note: when running the client, you will need to run the command `python3 Starcraft2Client.py`. +To run StarCraft 2 through Archipelago in macOS, you will need to run the client via source as seen here: [macOS Guide](/tutorial/Archipelago/mac/en). Note: to launch the client, you will need to run the command `python3 Starcraft2Client.py`. ## Running in Linux diff --git a/worlds/sc2/docs/setup_fr.md b/worlds/sc2/docs/setup_fr.md new file mode 100644 index 000000000000..bb6c35bce1c7 --- /dev/null +++ b/worlds/sc2/docs/setup_fr.md @@ -0,0 +1,214 @@ +# Guide d'installation du *StarCraft 2 Randomizer* + +Ce guide contient les instructions pour installer et dépanner le client de *StarCraft 2 Archipelago*, ainsi que des +indications pour obtenir un fichier de configuration de *StarCraft 2 Archipelago* et comment modifier ce dernier. + +## Logiciels requis + +- [*StarCraft 2*](https://starcraft2.com/en-us/) +- [La version la plus récente d'Archipelago](https://github.com/ArchipelagoMW/Archipelago/releases) + +## Comment est-ce que j'installe ce *randomizer*? + +1. Installer *StarCraft 2* et Archipelago en suivant les instructions indiquées dans les liens précédents. Le client de +*StarCraft 2 Archipelago* est téléchargé par le programme d'installation d'Archipelago. + - Les utilisateurs de Linux devraient aussi suivre les instructions qui se retrouvent à la fin de cette page +(["Exécuter sous Linux"](#exécuter-sous-linux)). + - Notez que votre jeu *StarCraft 2* doit être en anglais pour fonctionner avec Archipelago. +2. Exécuter `ArchipelagoStarcraft2Client.exe`. + - Uniquement pour cette étape, les utilisateurs de macOS devraient plutôt suivre les instructions qui se trouvent à +["Exécuter sous macOS"](#exécuter-sous-macos). +3. Dans le client de *StarCraft 2 Archipelago*, écrire la commande `/download_data`. Cette commande va lancer +l'installation des fichiers qui sont nécessaires pour jouer à *StarCraft 2 Archipelago*. + +## Où est-ce que j'obtiens le fichier de configuration (i.e., le *yaml*) pour ce jeu? + +Un fichier dans le format *yaml* est utilisé pour communiquer à Archipelago comment vous voulez que votre jeu soit +*randomized*. +Ce dernier est nécessaire même si vous voulez utiliser les options par défaut. +L'approche usuelle pour générer un *multiworld* consiste à avoir un fichier *yaml* par monde. + +Il y a trois approches pour obtenir un fichier *yaml* pour *StarCraft 2 Randomizer*: +* Vous pouvez aller à la page [*Player options*](/games/Starcraft%202/player-options) qui vous permet de définir vos +choix via une interface graphique et ensuite télécharger le *yaml* correspondant à ces choix. +* Vous pouvez obtenir le modèle de base en le téléchargeant à la page +[*Player options*](/games/Starcraft%202/player-options) ou en cliquant sur *Generate template* après avoir exécuté le +*Launcher* d'Archipelago (i.e., `ArchipelagoLauncher.exe`). Ce modèle de base inclut une description pour chacune des +options et vous n'avez qu'à modifier les options dans un éditeur de texte de votre choix. +* Vous pouvez demander à quelqu'un d'autre de partager un de ces fichiers *yaml* pour l'utiliser ou l'ajuster à vos +préférences. + +Prenez soin de vous rappeler du nom de joueur que vous avez inscrit dans la page à options ou dans le fichier *yaml* +puisque vous en aurez besoin pour vous connecter à votre monde! + +Notez que la page *Player options* ne permet pas de définir certaines des options avancées, e.g., l'exclusion de +certaines unités ou de leurs améliorations. +Utilisez la page [*Weighted Options*](/weighted-options) pour avoir accès à ces dernières. + +Si vous désirez des informations et/ou instructions générales sur l'utilisation d'un fichier *yaml* pour Archipelago, +veuillez consulter [*Creating a YAML*](/tutorial/Archipelago/setup/en#creating-a-yaml). + +### Questions récurrentes à propos du fichier *yaml* +#### Comment est-ce que je sais que mon *yaml* est bien défini? + +La manière la plus simple de valider votre *yaml* est d'utiliser le +[système de validation](/check) du site web. + +Vous pouvez aussi le tester en tentant de générer un *multiworld* avec votre *yaml*. +Pour faire ça, sauvegardez votre *yaml* dans le dossier `Players/` de votre installation d'Archipelago et exécutez +`ArchipelagoGenerate.exe`. +Si votre *yaml* est bien défini, vous devriez voir un nouveau fichier, avec l'extension `.zip`, apparaître dans le +dossier `output/` de votre installation d'Archipelago. +Il est recommandé de lancer `ArchipelagoGenerate.exe` via un terminal afin que vous puissiez voir les messages générés +par le logiciel, ce qui va inclure toutes erreurs qui ont eu lieu et le nom de fichier généré. +Si vous n'appréciez pas le fait d'utiliser un terminal, vous pouvez aussi regarder le fichier *log* qui va être produit +dans le dossier `logs/`. + +#### À quoi sert l'option *Progression Balancing*? + +Pour *Starcraft 2*, cette option ne fait pas grand-chose. +Il s'agit d'une option d'Archipelago permettant d'équilibrer la progression des mondes en interchangeant les *items* +dans les *spheres*. +Si le *Progression Balancing* d'un monde est plus grand que ceux des autres, les *items* de progression de ce monde ont +plus de chance d'être obtenus tôt et vice-versa si sa valeur est plus petite que celle des autres mondes. +Cependant, *Starcraft 2* est beaucoup plus permissif en termes d'*items* qui permettent de progresser, ce réglage à +donc peu d'influence sur la progression dans *StarCraft 2*. +Vu qu'il augmente le temps de génération d'un *MultiWorld*, nous recommandons de le désactiver, c-à-d le définir à +zéro, pour *Starcraft 2*. + + +#### Comment est-ce que je définis une liste d'*items*, e.g. pour l'option *excluded items*? + +Vous pouvez lire sur la syntaxe des conteneurs dans le format *yaml* à la page +[*YAML specification*](https://yaml.org/spec/1.2.2/#21-collections). +Pour les listes, chaque *item* doit être sur sa propre ligne et doit être précédé par un trait d'union. + +```yaml +excluded_items: + - Battlecruiser + - Drop-Pods (Kerrigan Tier 7) +``` + +Une liste vide est représentée par une paire de crochets: `[]`. +Il s'agit de la valeur par défaut dans le modèle de base, ce qui devrait vous aider à apprendre à utiliser cette +syntaxe. + +#### Comment est-ce que je fais pour avoir des *items* dès le départ? + +L'option *starting inventory* est un *map* et non une liste. +Ainsi, elle permet de spécifier le nombre de chaque *item* avec lequel vous allez commencer. +Sa syntaxe consiste à indiquer le nom de l'*item*, suivi par un deux-points, puis par un espace et enfin par le nombre +désiré de cet *item*. + +```yaml +start_inventory: + Micro-Filtering: 1 + Additional Starting Vespene: 5 +``` + +Un *map* vide est représenté par une paire d'accolades: `{}`. +Il s'agit de la valeur par défaut dans le modèle de base, ce qui devrait vous aider à apprendre à utiliser cette +syntaxe. + +#### Comment est-ce que je fais pour connaître le nom des *items* et des *locations* dans *StarCraft 2 Archipelago*? + +La page [*datapackage*](/datapackage) d'Archipelago liste l'ensemble des *items* et des *locations* de tous les jeux +que le site web prend en charge actuellement, dont ceux de *StarCraft 2*. + +Vous trouverez aussi la liste complète des *items* de *StarCraft 2 Archipelago* à la page +[*Icon Repository*](https://matthewmarinets.github.io/ap_sc2_icons/). +Notez que cette page contient diverses informations supplémentaires sur chacun des *items*. +Cependant, l'information présente dans cette dernière peut différer de celle du *datapackage* d'Archipelago +puisqu'elle est générée, habituellement, à partir de la version en développement de *StarCraft 2 Archipelago* qui +n'ont peut-être pas encore été inclus dans le site web d'Archipelago. + +## Comment est-ce que je peux joindre un *MultiWorld*? + +1. Exécuter `ArchipelagoStarcraft2Client.exe`. + - Uniquement pour cette étape, les utilisateurs de macOS devraient plutôt suivre les instructions à la page +["Exécuter sous macOS"](#exécuter-sous-macos). +2. Entrer la commande `/connect [server ip]`. + - Si le *MultiWorld* est hébergé via un siteweb, l'IP du server devrait être indiqué dans le haut de la page de +votre *room*. +3. Inscrivez le nom de joueur spécifié dans votre *yaml* lorsque vous y êtes invité. +4. Si le serveur a un mot de passe, l'inscrire lorsque vous y êtes invité. +5. Une fois connecté, aller sur l'onglet *StarCraft 2 Launcher* dans le client. Dans cet onglet, vous devriez trouver +toutes les missions de votre monde. Les missions qui ne sont pas disponibles présentement auront leur texte dans une +nuance de gris. Vous n'avez qu'à cliquer une des missions qui est disponible pour la commencer! + +## *StarCraft 2* ne démarre pas quand je tente de commencer une mission + +Pour commencer, regarder le fichier *log* pour trouver le problème (ce dernier devrait être dans +`[Archipelago Directory]/logs/SC2Client.txt`). +Si vous ne comprenez pas le problème avec le fichier *log*, visitez notre +[*Discord*](https://discord.com/invite/8Z65BR2) pour demander de l'aide dans le forum *tech-support*. +Dans votre message, veuillez inclure une description détaillée de ce qui ne marche pas et ajouter en pièce jointe le +fichier *log*. + +## Mon profil de raccourcis clavier n'est pas disponibles quand je joue à *StarCraft 2 Archipelago* + +Pour que votre profil de raccourcis clavier fonctionne dans Archipelago, vous devez copier votre fichier de raccourcis +qui se trouve dans `Documents/StarCraft II/Accounts/######/Hotkeys` vers `Documents/StarCraft II/Hotkeys`. +Si le dossier n'existe pas, créez-le. + +Pour que *StarCraft 2 Archipelago* utilise votre profil, suivez les étapes suivantes. +Lancez *Starcraft 2* via l'application *Battle.net*. +Changez votre profil de raccourcis clavier pour le mode standard et acceptez, puis sélectionnez votre profil +personnalisé et acceptez. +Vous n'aurez besoin de faire ça qu'une seule fois. + +## Exécuter sous macOS + +Pour exécuter *StarCraft 2* via Archipelago sous macOS, vous devez exécuter le client à partir de la source +comme indiqué ici: [*macOS Guide*](/tutorial/Archipelago/mac/en). +Notez que pour lancer le client, vous devez exécuter la commande `python3 Starcraft2Client.py`. + +## Exécuter sous Linux + +Pour exécuter *StarCraft 2* via Archipelago sous Linux, vous allez devoir installer le jeu avec *Wine* et ensuite +exécuter le client d'Archipelago pour Linux. + +Confirmez que vous avez installé *StarCraft 2* via *Wine* et que vous avez suivi les +[instructions d'installation](#comment-est-ce-que-j'installe-ce-randomizer?) pour ajouter les *Maps* et les *Data +files* nécessairent pour *StarCraft 2 Archipelago* au bon endroit. +Vous n'avez pas besoin de copier les fichiers `.dll`. +Si vous avez des difficultés pour installer ou exécuter *StarCraft 2* sous Linux, il est recommandé d'utiliser le +logiciel *Lutris*. + +Copier ce qui suit dans un fichier avec l'extension `.sh`, en prenant soin de définir les variables **WINE** et +**SC2PATH** avec les bons chemins et de définir **PATH_TO_ARCHIPELAGO** avec le chemin vers le dossier qui contient le +*AppImage* si ce dernier n'est pas dans le même dossier que ce script. + +```sh +# Permet au client de savoir que SC2 est exécuté via Wine +export SC2PF=WineLinux +export PROTOCOL_BUFFERS_PYTHON_IMPLEMENTATION=python + +# À_CHANGER Remplacer le chemin avec celui qui correspond à la version de Wine utilisé pour exécuter SC2 +export WINE="/usr/bin/wine" + +# À_CHANGER Remplacer le chemin par celui qui indique où StarCraft II est installé +export SC2PATH="/home/user/Games/starcraft-ii/drive_c/Program Files (x86)/StarCraft II/" + +# À_CHANGER Indiquer le dossier qui contient l'AppImage d'Archipelago +PATH_TO_ARCHIPELAGO= + +# Obtiens la dernière version de l'AppImage de Archipelago dans le dossier PATH_TO_ARCHIPELAGO. +# Si PATH_TO_ARCHIPELAGO n'est pas défini, la valeur par défaut est le dossier qui contient ce script. +ARCHIPELAGO="$(ls ${PATH_TO_ARCHIPELAGO:-$(dirname $0)}/Archipelago_*.AppImage | sort -r | head -1)" + +# Lance le client de Archipelago +$ARCHIPELAGO Starcraft2Client +``` + +Pour une installation via Lutris, vous pouvez exécuter `lutris -l` pour obtenir l'identifiant numérique de votre +installation *StarCraft II* et ensuite exécuter la commande suivante, en remplacant **${ID}** pour cet identifiant +numérique. + + lutris lutris:rungameid/${ID} --output-script sc2.sh + +Cette commande va définir toutes les variables d'environnement nécessaires pour exécuter *StarCraft 2* dans un script, +incluant le chemin vers l'exécutable *Wine* que Lutris utilise. +Après ça, vous pouvez enlever la ligne qui permet de démarrer *Battle.Net* et copier le code décrit plus haut dans le +script produit. + From e31a7093de004b062b76f8ebb6b115740c7b86e8 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Wed, 29 May 2024 16:53:18 +0200 Subject: [PATCH 31/64] WebHost: use settings defaults for /api/generate and options -> Single Player Generate (#3411) --- WebHostLib/generate.py | 28 +++++++++++++--------------- WebHostLib/options.py | 3 ++- settings.py | 11 ----------- 3 files changed, 15 insertions(+), 27 deletions(-) diff --git a/WebHostLib/generate.py b/WebHostLib/generate.py index a78560cb0bd3..a12dc0f4ae14 100644 --- a/WebHostLib/generate.py +++ b/WebHostLib/generate.py @@ -6,7 +6,7 @@ import tempfile import zipfile from collections import Counter -from typing import Any, Dict, List, Optional, Union +from typing import Any, Dict, List, Optional, Union, Set from flask import flash, redirect, render_template, request, session, url_for from pony.orm import commit, db_session @@ -16,6 +16,7 @@ from Main import main as ERmain from Utils import __version__ from WebHostLib import app +from settings import ServerOptions, GeneratorOptions from worlds.alttp.EntranceRandomizer import parse_arguments from .check import get_yaml_data, roll_options from .models import Generation, STATE_ERROR, STATE_QUEUED, Seed, UUID @@ -23,25 +24,22 @@ def get_meta(options_source: dict, race: bool = False) -> Dict[str, Union[List[str], Dict[str, Any]]]: - plando_options = { - options_source.get("plando_bosses", ""), - options_source.get("plando_items", ""), - options_source.get("plando_connections", ""), - options_source.get("plando_texts", "") - } - plando_options -= {""} + plando_options: Set[str] = set() + for substr in ("bosses", "items", "connections", "texts"): + if options_source.get(f"plando_{substr}", substr in GeneratorOptions.plando_options): + plando_options.add(substr) server_options = { - "hint_cost": int(options_source.get("hint_cost", 10)), - "release_mode": options_source.get("release_mode", "goal"), - "remaining_mode": options_source.get("remaining_mode", "disabled"), - "collect_mode": options_source.get("collect_mode", "disabled"), - "item_cheat": bool(int(options_source.get("item_cheat", 1))), + "hint_cost": int(options_source.get("hint_cost", ServerOptions.hint_cost)), + "release_mode": options_source.get("release_mode", ServerOptions.release_mode), + "remaining_mode": options_source.get("remaining_mode", ServerOptions.remaining_mode), + "collect_mode": options_source.get("collect_mode", ServerOptions.collect_mode), + "item_cheat": bool(int(options_source.get("item_cheat", not ServerOptions.disable_item_cheat))), "server_password": options_source.get("server_password", None), } generator_options = { - "spoiler": int(options_source.get("spoiler", 0)), - "race": race + "spoiler": int(options_source.get("spoiler", GeneratorOptions.spoiler)), + "race": race, } if race: diff --git a/WebHostLib/options.py b/WebHostLib/options.py index 4a791135d7c6..1026d7638502 100644 --- a/WebHostLib/options.py +++ b/WebHostLib/options.py @@ -11,6 +11,7 @@ from Utils import local_path from worlds.AutoWorld import AutoWorldRegister from . import app, cache +from .generate import get_meta def create() -> None: @@ -50,7 +51,7 @@ def render_options_page(template: str, world_name: str, is_complex: bool = False def generate_game(options: Dict[str, Union[dict, str]]) -> Union[Response, str]: from .generate import start_generation - return start_generation(options, {"plando_options": ["items", "connections", "texts", "bosses"]}) + return start_generation(options, get_meta({})) def send_yaml(player_name: str, formatted_options: dict) -> Response: diff --git a/settings.py b/settings.py index 9d1c0904ddd8..7ab618c344d8 100644 --- a/settings.py +++ b/settings.py @@ -643,17 +643,6 @@ class Spoiler(IntEnum): PLAYTHROUGH = 2 FULL = 3 - class GlitchTriforceRoom(IntEnum): - """ - Glitch to Triforce room from Ganon - When disabled, you have to have a weapon that can hurt ganon (master sword or swordless/easy item functionality - + hammer) and have completed the goal required for killing ganon to be able to access the triforce room. - 1 -> Enabled. - 0 -> Disabled (except in no-logic) - """ - OFF = 0 - ON = 1 - class PlandoOptions(str): """ List of options that can be plando'd. Can be combined, for example "bosses, items" From 34f903e97a2dc7f14124d64f6a21ad04685f525a Mon Sep 17 00:00:00 2001 From: Zach Parks Date: Wed, 29 May 2024 09:59:40 -0500 Subject: [PATCH 32/64] CODEOWNERS: Remove @jtoyoda as world maintainer for Final Fantasy (#3398) --- docs/CODEOWNERS | 28 +++++++++++++++++----------- 1 file changed, 17 insertions(+), 11 deletions(-) diff --git a/docs/CODEOWNERS b/docs/CODEOWNERS index 068c7240578d..f54132e24aa0 100644 --- a/docs/CODEOWNERS +++ b/docs/CODEOWNERS @@ -6,10 +6,6 @@ # # All usernames must be GitHub usernames (and are case sensitive). -################### -## Active Worlds ## -################### - # Adventure /worlds/adventure/ @JusticePS @@ -67,9 +63,6 @@ # Factorio /worlds/factorio/ @Berserker66 -# Final Fantasy -/worlds/ff1/ @jtoyoda - # Final Fantasy Mystic Quest /worlds/ffmq/ @Alchav @wildham0 @@ -215,9 +208,22 @@ # Zork Grand Inquisitor /worlds/zork_grand_inquisitor/ @nbrochu -################################## -## Disabled Unmaintained Worlds ## -################################## + +## Active Unmaintained Worlds + +# The following worlds in this repo are currently unmaintained, but currently still work in core. If any update breaks +# compatibility, these worlds may be moved to `worlds_disabled`. If you are interested in stepping up as maintainer for +# any of these worlds, please review `/docs/world maintainer.md` documentation. + +# Final Fantasy (1) +# /worlds/ff1/ + + +## Disabled Unmaintained Worlds + +# The following worlds in this repo are currently unmaintained and disabled as they do not work in core. If you are +# interested in stepping up as maintainer for any of these worlds, please review `/docs/world maintainer.md` +# documentation. # Ori and the Blind Forest -# /worlds_disabled/oribf/ +# /worlds_disabled/oribf/ From 378af4b07c65f36c1f0d7cf437d89a45e81acb74 Mon Sep 17 00:00:00 2001 From: Witchybun <96719127+Witchybun@users.noreply.github.com> Date: Wed, 29 May 2024 13:16:19 -0500 Subject: [PATCH 33/64] Stardew Valley: Fix magic altar logic (#3417) * Fix magic altar logic * Force a tuple (really?) * Fix received and force progression on all spells * Reversing the tuple change (?yllaer) --- worlds/stardew_valley/data/items.csv | 18 +++---- .../stardew_valley/mods/logic/magic_logic.py | 5 +- worlds/stardew_valley/strings/spells.py | 50 +++++++++++-------- 3 files changed, 41 insertions(+), 32 deletions(-) diff --git a/worlds/stardew_valley/data/items.csv b/worlds/stardew_valley/data/items.csv index a3096cf789df..9ecb2ba3649e 100644 --- a/worlds/stardew_valley/data/items.csv +++ b/worlds/stardew_valley/data/items.csv @@ -735,26 +735,26 @@ id,name,classification,groups,mod_name 10007,Tractor Garage,useful,,Tractor Mod 10008,Woods Obelisk,progression,,DeepWoods 10009,Spell: Clear Debris,progression,MAGIC_SPELL,Magic -10010,Spell: Till,useful,MAGIC_SPELL,Magic +10010,Spell: Till,progression,MAGIC_SPELL,Magic 10011,Spell: Water,progression,MAGIC_SPELL,Magic 10012,Spell: Blink,progression,MAGIC_SPELL,Magic -10013,Spell: Evac,useful,MAGIC_SPELL,Magic -10014,Spell: Haste,useful,MAGIC_SPELL,Magic +10013,Spell: Evac,progression,MAGIC_SPELL,Magic +10014,Spell: Haste,progression,MAGIC_SPELL,Magic 10015,Spell: Heal,progression,MAGIC_SPELL,Magic -10016,Spell: Buff,useful,MAGIC_SPELL,Magic +10016,Spell: Buff,progression,MAGIC_SPELL,Magic 10017,Spell: Shockwave,progression,MAGIC_SPELL,Magic 10018,Spell: Fireball,progression,MAGIC_SPELL,Magic 10019,Spell: Frostbolt,progression,MAGIC_SPELL,Magic 10020,Spell: Teleport,progression,MAGIC_SPELL,Magic -10021,Spell: Lantern,useful,MAGIC_SPELL,Magic +10021,Spell: Lantern,progression,MAGIC_SPELL,Magic 10022,Spell: Tendrils,progression,MAGIC_SPELL,Magic -10023,Spell: Photosynthesis,useful,MAGIC_SPELL,Magic +10023,Spell: Photosynthesis,progression,MAGIC_SPELL,Magic 10024,Spell: Descend,progression,MAGIC_SPELL,Magic 10025,Spell: Meteor,progression,MAGIC_SPELL,Magic -10026,Spell: Bloodmana,useful,MAGIC_SPELL,Magic -10027,Spell: Lucksteal,useful,MAGIC_SPELL,Magic +10026,Spell: Bloodmana,progression,MAGIC_SPELL,Magic +10027,Spell: Lucksteal,progression,MAGIC_SPELL,Magic 10028,Spell: Spirit,progression,MAGIC_SPELL,Magic -10029,Spell: Rewind,useful,MAGIC_SPELL,Magic +10029,Spell: Rewind,progression,MAGIC_SPELL,Magic 10030,Pendant of Community,progression,,DeepWoods 10031,Pendant of Elders,progression,,DeepWoods 10032,Pendant of Depths,progression,,DeepWoods diff --git a/worlds/stardew_valley/mods/logic/magic_logic.py b/worlds/stardew_valley/mods/logic/magic_logic.py index 99482b063056..662ff3acaeb6 100644 --- a/worlds/stardew_valley/mods/logic/magic_logic.py +++ b/worlds/stardew_valley/mods/logic/magic_logic.py @@ -8,7 +8,7 @@ from ...stardew_rule import StardewRule, False_ from ...strings.ap_names.skill_level_names import ModSkillLevel from ...strings.region_names import MagicRegion -from ...strings.spells import MagicSpell +from ...strings.spells import MagicSpell, all_spells class MagicLogicMixin(BaseLogicMixin): @@ -27,7 +27,8 @@ def can_use_clear_debris_instead_of_tool_level(self, level: int) -> StardewRule: def can_use_altar(self) -> StardewRule: if ModNames.magic not in self.options.mods: return False_() - return self.logic.region.can_reach(MagicRegion.altar) + spell_rule = False_() + return self.logic.region.can_reach(MagicRegion.altar) & self.logic.received_any(*all_spells) def has_any_spell(self) -> StardewRule: if ModNames.magic not in self.options.mods: diff --git a/worlds/stardew_valley/strings/spells.py b/worlds/stardew_valley/strings/spells.py index ef5545c56902..4b246c173a49 100644 --- a/worlds/stardew_valley/strings/spells.py +++ b/worlds/stardew_valley/strings/spells.py @@ -1,22 +1,30 @@ +all_spells = [] + + +def spell(name: str) -> str: + all_spells.append(name) + return name + + class MagicSpell: - clear_debris = "Spell: Clear Debris" - till = "Spell: Till" - water = "Spell: Water" - blink = "Spell: Blink" - evac = "Spell: Evac" - haste = "Spell: Haste" - heal = "Spell: Heal" - buff = "Spell: Buff" - shockwave = "Spell: Shockwave" - fireball = "Spell: Fireball" - frostbite = "Spell: Frostbolt" - teleport = "Spell: Teleport" - lantern = "Spell: Lantern" - tendrils = "Spell: Tendrils" - photosynthesis = "Spell: Photosynthesis" - descend = "Spell: Descend" - meteor = "Spell: Meteor" - bloodmana = "Spell: Bloodmana" - lucksteal = "Spell: Lucksteal" - spirit = "Spell: Spirit" - rewind = "Spell: Rewind" + clear_debris = spell("Spell: Clear Debris") + till = spell("Spell: Till") + water = spell("Spell: Water") + blink = spell("Spell: Blink") + evac = spell("Spell: Evac") + haste = spell("Spell: Haste") + heal = spell("Spell: Heal") + buff = spell("Spell: Buff") + shockwave = spell("Spell: Shockwave") + fireball = spell("Spell: Fireball") + frostbite = spell("Spell: Frostbolt") + teleport = spell("Spell: Teleport") + lantern = spell("Spell: Lantern") + tendrils = spell("Spell: Tendrils") + photosynthesis = spell("Spell: Photosynthesis") + descend = spell("Spell: Descend") + meteor = spell("Spell: Meteor") + bloodmana = spell("Spell: Bloodmana") + lucksteal = spell("Spell: Lucksteal") + spirit = spell("Spell: Spirit") + rewind = spell("Spell: Rewind") From 6f6bf3c62d4411b41570ffc0728d5c937ed4cafb Mon Sep 17 00:00:00 2001 From: black-sliver <59490463+black-sliver@users.noreply.github.com> Date: Thu, 30 May 2024 18:16:13 +0200 Subject: [PATCH 34/64] CustomServer: properly 'inherit' Archipelago from static_server_data (#3366) This fixes a potential exception during room spin-up. --- WebHostLib/customserver.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/WebHostLib/customserver.py b/WebHostLib/customserver.py index 16769b7a760e..3a86cb551d27 100644 --- a/WebHostLib/customserver.py +++ b/WebHostLib/customserver.py @@ -106,9 +106,9 @@ def load(self, room_id: int): static_gamespackage = self.gamespackage # this is shared across all rooms static_item_name_groups = self.item_name_groups static_location_name_groups = self.location_name_groups - self.gamespackage = {"Archipelago": static_gamespackage["Archipelago"]} # this may be modified by _load - self.item_name_groups = {} - self.location_name_groups = {} + self.gamespackage = {"Archipelago": static_gamespackage.get("Archipelago", {})} # this may be modified by _load + self.item_name_groups = {"Archipelago": static_item_name_groups.get("Archipelago", {})} + self.location_name_groups = {"Archipelago": static_location_name_groups.get("Archipelago", {})} for game in list(multidata.get("datapackage", {})): game_data = multidata["datapackage"][game] From 2fe8c433510831429ecef766accfec03af01c2f8 Mon Sep 17 00:00:00 2001 From: Salzkorn Date: Thu, 30 May 2024 18:52:01 +0200 Subject: [PATCH 35/64] SC2: Fix Kerrigan Primal Form on Half Completion (#3419) --- worlds/sc2/Client.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/worlds/sc2/Client.py b/worlds/sc2/Client.py index 96b3ddc66b44..4e55509dda48 100644 --- a/worlds/sc2/Client.py +++ b/worlds/sc2/Client.py @@ -966,8 +966,8 @@ def kerrigan_primal(ctx: SC2Context, kerrigan_level: int) -> bool: return kerrigan_level >= 35 elif ctx.kerrigan_primal_status == KerriganPrimalStatus.option_half_completion: total_missions = len(ctx.mission_id_to_location_ids) - completed = len([(mission_id * VICTORY_MODULO + get_location_offset(mission_id)) in ctx.checked_locations - for mission_id in ctx.mission_id_to_location_ids]) + completed = sum((mission_id * VICTORY_MODULO + get_location_offset(mission_id)) in ctx.checked_locations + for mission_id in ctx.mission_id_to_location_ids) return completed >= (total_missions / 2) elif ctx.kerrigan_primal_status == KerriganPrimalStatus.option_item: codes = [item.item for item in ctx.items_received] From 7058575c9502a4a35bb8e75fae767f2269c576c4 Mon Sep 17 00:00:00 2001 From: BadMagic100 Date: Thu, 30 May 2024 10:57:54 -0700 Subject: [PATCH 36/64] Hollow Knight: Add missing comma (#3403) --- worlds/hk/Options.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worlds/hk/Options.py b/worlds/hk/Options.py index f7b4420c7447..f408528821cc 100644 --- a/worlds/hk/Options.py +++ b/worlds/hk/Options.py @@ -105,7 +105,7 @@ "RandomizeVesselFragments", "RandomizeCharmNotches", "RandomizePaleOre", - "RandomizeRancidEggs" + "RandomizeRancidEggs", "RandomizeRelics", "RandomizeStags", "RandomizeLifebloodCocoons" From b055a394547a1180d1cadd370b4cd072455ae9e8 Mon Sep 17 00:00:00 2001 From: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> Date: Fri, 31 May 2024 15:48:21 -0400 Subject: [PATCH 37/64] PKMN R/B: "J.r" -> "Jr." (#3423) --- worlds/pokemon_rb/locations.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worlds/pokemon_rb/locations.py b/worlds/pokemon_rb/locations.py index b7b7e533a5ee..251beb59cc18 100644 --- a/worlds/pokemon_rb/locations.py +++ b/worlds/pokemon_rb/locations.py @@ -636,7 +636,7 @@ def __init__(self, flag): LocationData("Rock Tunnel B1F-W", "PokeManiac 3", None, rom_addresses["Trainersanity_EVENT_BEAT_ROCK_TUNNEL_2_TRAINER_2_ITEM"], EventFlag(11), inclusion=trainersanity), LocationData("Route 10-N", "Jr. Trainer F 1", None, rom_addresses["Trainersanity_EVENT_BEAT_ROUTE_10_TRAINER_3_ITEM"], EventFlag(308), inclusion=trainersanity), LocationData("Route 10-C", "PokeManiac 1", None, rom_addresses["Trainersanity_EVENT_BEAT_ROUTE_10_TRAINER_0_ITEM"], EventFlag(311), inclusion=trainersanity), - LocationData("Route 10-S", "J.r Trainer F 2", None, rom_addresses["Trainersanity_EVENT_BEAT_ROUTE_10_TRAINER_5_ITEM"], EventFlag(306), inclusion=trainersanity), + LocationData("Route 10-S", "Jr. Trainer F 2", None, rom_addresses["Trainersanity_EVENT_BEAT_ROUTE_10_TRAINER_5_ITEM"], EventFlag(306), inclusion=trainersanity), LocationData("Route 10-S", "Hiker 1", None, rom_addresses["Trainersanity_EVENT_BEAT_ROUTE_10_TRAINER_1_ITEM"], EventFlag(310), inclusion=trainersanity), LocationData("Route 10-S", "Hiker 2", None, rom_addresses["Trainersanity_EVENT_BEAT_ROUTE_10_TRAINER_4_ITEM"], EventFlag(307), inclusion=trainersanity), LocationData("Route 10-S", "PokeManiac 2", None, rom_addresses["Trainersanity_EVENT_BEAT_ROUTE_10_TRAINER_2_ITEM"], EventFlag(309), inclusion=trainersanity), From 15e06e1779ed97ec2609933bea46da91e97a5679 Mon Sep 17 00:00:00 2001 From: Chris Wilson Date: Fri, 31 May 2024 22:41:03 -0400 Subject: [PATCH 38/64] Fix TextChoice options sometimes creating a broken YAML (#3390) * Fix TextChoice options with custom values improperly being included in YAML output * Update WebHostLib/options.py Co-authored-by: Fabian Dill --------- Co-authored-by: Fabian Dill --- WebHostLib/options.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/WebHostLib/options.py b/WebHostLib/options.py index 1026d7638502..b112c8e361b1 100644 --- a/WebHostLib/options.py +++ b/WebHostLib/options.py @@ -169,9 +169,9 @@ def generate_yaml(game: str): else: options[key] = val - # Detect and build ItemDict options from their name pattern for key, val in options.copy().items(): key_parts = key.rsplit("||", 2) + # Detect and build ItemDict options from their name pattern if key_parts[-1] == "qty": if key_parts[0] not in options: options[key_parts[0]] = {} @@ -179,6 +179,13 @@ def generate_yaml(game: str): options[key_parts[0]][key_parts[1]] = int(val) del options[key] + # Detect keys which end with -custom, indicating a TextChoice with a possible custom value + elif key_parts[-1].endswith("-custom"): + if val: + options[key_parts[-1][:-7]] = val + + del options[key] + # Detect random-* keys and set their options accordingly for key, val in options.copy().items(): if key.startswith("random-"): From f3003ff147b38f696c62ac7cc26d96d5bd6c51bc Mon Sep 17 00:00:00 2001 From: Chris Wilson Date: Fri, 31 May 2024 22:41:49 -0400 Subject: [PATCH 39/64] Fix options pages sometimes displaying blank values in form fields (#3364) --- WebHostLib/options.py | 30 +++++++++++++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/WebHostLib/options.py b/WebHostLib/options.py index b112c8e361b1..62ba86a56626 100644 --- a/WebHostLib/options.py +++ b/WebHostLib/options.py @@ -76,6 +76,34 @@ def test_ordered(obj): def option_presets(game: str) -> Response: world = AutoWorldRegister.world_types[game] + presets = {} + for preset_name, preset in world.web.options_presets.items(): + presets[preset_name] = {} + for preset_option_name, preset_option in preset.items(): + if preset_option == "random": + presets[preset_name][preset_option_name] = preset_option + continue + + option = world.options_dataclass.type_hints[preset_option_name].from_any(preset_option) + if isinstance(option, Options.NamedRange) and isinstance(preset_option, str): + assert preset_option in option.special_range_names, \ + f"Invalid preset value '{preset_option}' for '{preset_option_name}' in '{preset_name}'. " \ + f"Expected {option.special_range_names.keys()} or {option.range_start}-{option.range_end}." + + presets[preset_name][preset_option_name] = option.value + elif isinstance(option, Options.Range): + presets[preset_name][preset_option_name] = option.value + elif isinstance(preset_option, str): + # Ensure the option value is valid for Choice and Toggle options + assert option.name_lookup[option.value] == preset_option, \ + f"Invalid option value '{preset_option}' for '{preset_option_name}' in preset '{preset_name}'. " \ + f"Values must not be resolved to a different option via option.from_text (or an alias)." + # Use the name of the option + presets[preset_name][preset_option_name] = option.current_key + else: + # Use the name of the option + presets[preset_name][preset_option_name] = option.current_key + class SetEncoder(json.JSONEncoder): def default(self, obj): from collections.abc import Set @@ -83,7 +111,7 @@ def default(self, obj): return list(obj) return json.JSONEncoder.default(self, obj) - json_data = json.dumps(world.web.options_presets, cls=SetEncoder) + json_data = json.dumps(presets, cls=SetEncoder) response = Response(json_data) response.headers["Content-Type"] = "application/json" return response From 5aa6ad63ca9da9aa8ea40831522e1d89726c6ce0 Mon Sep 17 00:00:00 2001 From: Zach Parks Date: Sat, 1 Jun 2024 06:07:13 -0500 Subject: [PATCH 40/64] Core: Remove Universally Unique ID Requirements (Per-Game Data Packages) (#1933) --- AdventureClient.py | 2 +- CommonClient.py | 91 ++++++++++++-- MultiServer.py | 50 ++++---- NetUtils.py | 6 +- UndertaleClient.py | 4 +- Utils.py | 5 +- WargrooveClient.py | 6 +- WebHostLib/api/__init__.py | 9 -- WebHostLib/templates/ootTracker.html | 180 --------------------------- Zelda1Client.py | 4 +- docs/network protocol.md | 58 ++++++--- kvui.py | 14 ++- test/general/test_ids.py | 16 --- test/programs/test_common_client.py | 106 ++++++++++++++++ worlds/AutoWorld.py | 13 -- worlds/__init__.py | 1 - worlds/adventure/__init__.py | 1 - worlds/alttp/Client.py | 6 +- worlds/alttp/__init__.py | 1 - worlds/bk_sudoku/__init__.py | 1 - worlds/blasphemous/__init__.py | 1 - worlds/bumpstik/__init__.py | 2 - worlds/checksfinder/__init__.py | 2 - worlds/clique/__init__.py | 1 - worlds/cv64/__init__.py | 1 - worlds/cv64/client.py | 2 +- worlds/dark_souls_3/__init__.py | 1 - worlds/dkc3/Client.py | 6 +- worlds/dkc3/__init__.py | 1 - worlds/dlcquest/__init__.py | 2 - worlds/doom_1993/__init__.py | 1 - worlds/doom_ii/__init__.py | 1 - worlds/factorio/Client.py | 4 +- worlds/factorio/__init__.py | 1 - worlds/ff1/__init__.py | 1 - worlds/ffmq/__init__.py | 3 - worlds/generic/__init__.py | 1 - worlds/heretic/__init__.py | 1 - worlds/hk/__init__.py | 1 - worlds/hylics2/__init__.py | 2 - worlds/kdl3/Client.py | 6 +- worlds/ladx/__init__.py | 5 - worlds/lingo/__init__.py | 1 - worlds/lufia2ac/Client.py | 4 +- worlds/lufia2ac/__init__.py | 1 - worlds/meritous/__init__.py | 2 - worlds/minecraft/__init__.py | 2 - worlds/mmbn3/__init__.py | 2 - worlds/noita/__init__.py | 3 +- worlds/oot/__init__.py | 2 - worlds/overcooked2/__init__.py | 1 - worlds/pokemon_emerald/__init__.py | 1 - worlds/pokemon_rb/__init__.py | 1 - worlds/raft/__init__.py | 1 - worlds/rogue_legacy/__init__.py | 1 - worlds/ror2/__init__.py | 1 - worlds/sa2b/__init__.py | 1 - worlds/sc2/Client.py | 6 +- worlds/sc2/ClientGui.py | 2 +- worlds/shorthike/__init__.py | 1 - worlds/sm/Client.py | 7 +- worlds/sm/__init__.py | 1 - worlds/sm64ex/__init__.py | 1 - worlds/smw/Client.py | 13 +- worlds/smz3/Client.py | 7 +- worlds/smz3/__init__.py | 1 - worlds/soe/__init__.py | 1 - worlds/spire/__init__.py | 1 - worlds/stardew_valley/__init__.py | 1 - worlds/subnautica/__init__.py | 1 - worlds/terraria/__init__.py | 5 - worlds/timespinner/__init__.py | 3 +- worlds/tloz/__init__.py | 1 - worlds/undertale/__init__.py | 2 - worlds/v6/__init__.py | 2 - worlds/yoshisisland/Client.py | 6 +- worlds/zillion/__init__.py | 5 - 77 files changed, 319 insertions(+), 392 deletions(-) delete mode 100644 WebHostLib/templates/ootTracker.html create mode 100644 test/programs/test_common_client.py diff --git a/AdventureClient.py b/AdventureClient.py index 06e4d60dad43..7bfbd5ef6bd3 100644 --- a/AdventureClient.py +++ b/AdventureClient.py @@ -112,7 +112,7 @@ def on_package(self, cmd: str, args: dict): if ': !' not in msg: self._set_message(msg, SYSTEM_MESSAGE_ID) elif cmd == "ReceivedItems": - msg = f"Received {', '.join([self.item_names[item.item] for item in args['items']])}" + msg = f"Received {', '.join([self.item_names.lookup_in_slot(item.item) for item in args['items']])}" self._set_message(msg, SYSTEM_MESSAGE_ID) elif cmd == "Retrieved": if f"adventure_{self.auth}_freeincarnates_used" in args["keys"]: diff --git a/CommonClient.py b/CommonClient.py index 63cac098e22a..8af822cba571 100644 --- a/CommonClient.py +++ b/CommonClient.py @@ -1,5 +1,6 @@ from __future__ import annotations +import collections import copy import logging import asyncio @@ -8,6 +9,7 @@ import typing import time import functools +import warnings import ModuleUpdate ModuleUpdate.update() @@ -173,10 +175,74 @@ class CommonContext: items_handling: typing.Optional[int] = None want_slot_data: bool = True # should slot_data be retrieved via Connect - # data package - # Contents in flux until connection to server is made, to download correct data for this multiworld. - item_names: typing.Dict[int, str] = Utils.KeyedDefaultDict(lambda code: f'Unknown item (ID:{code})') - location_names: typing.Dict[int, str] = Utils.KeyedDefaultDict(lambda code: f'Unknown location (ID:{code})') + class NameLookupDict: + """A specialized dict, with helper methods, for id -> name item/location data package lookups by game.""" + def __init__(self, ctx: CommonContext, lookup_type: typing.Literal["item", "location"]): + self.ctx: CommonContext = ctx + self.lookup_type: typing.Literal["item", "location"] = lookup_type + self._unknown_item: typing.Callable[[int], str] = lambda key: f"Unknown {lookup_type} (ID: {key})" + self._archipelago_lookup: typing.Dict[int, str] = {} + self._flat_store: typing.Dict[int, str] = Utils.KeyedDefaultDict(self._unknown_item) + self._game_store: typing.Dict[str, typing.ChainMap[int, str]] = collections.defaultdict( + lambda: collections.ChainMap(self._archipelago_lookup, Utils.KeyedDefaultDict(self._unknown_item))) + self.warned: bool = False + + # noinspection PyTypeChecker + def __getitem__(self, key: str) -> typing.Mapping[int, str]: + # TODO: In a future version (0.6.0?) this should be simplified by removing implicit id lookups support. + if isinstance(key, int): + if not self.warned: + # Use warnings instead of logger to avoid deprecation message from appearing on user side. + self.warned = True + warnings.warn(f"Implicit name lookup by id only is deprecated and only supported to maintain " + f"backwards compatibility for now. If multiple games share the same id for a " + f"{self.lookup_type}, name could be incorrect. Please use " + f"`{self.lookup_type}_names.lookup_in_game()` or " + f"`{self.lookup_type}_names.lookup_in_slot()` instead.") + return self._flat_store[key] # type: ignore + + return self._game_store[key] + + def __len__(self) -> int: + return len(self._game_store) + + def __iter__(self) -> typing.Iterator[str]: + return iter(self._game_store) + + def __repr__(self) -> str: + return self._game_store.__repr__() + + def lookup_in_game(self, code: int, game_name: typing.Optional[str] = None) -> str: + """Returns the name for an item/location id in the context of a specific game or own game if `game` is + omitted. + """ + if game_name is None: + game_name = self.ctx.game + assert game_name is not None, f"Attempted to lookup {self.lookup_type} with no game name available." + + return self._game_store[game_name][code] + + def lookup_in_slot(self, code: int, slot: typing.Optional[int] = None) -> str: + """Returns the name for an item/location id in the context of a specific slot or own slot if `slot` is + omitted. + """ + if slot is None: + slot = self.ctx.slot + assert slot is not None, f"Attempted to lookup {self.lookup_type} with no slot info available." + + return self.lookup_in_game(code, self.ctx.slot_info[slot].game) + + def update_game(self, game: str, name_to_id_lookup_table: typing.Dict[str, int]) -> None: + """Overrides existing lookup tables for a particular game.""" + id_to_name_lookup_table = Utils.KeyedDefaultDict(self._unknown_item) + id_to_name_lookup_table.update({code: name for name, code in name_to_id_lookup_table.items()}) + self._game_store[game] = collections.ChainMap(self._archipelago_lookup, id_to_name_lookup_table) + self._flat_store.update(id_to_name_lookup_table) # Only needed for legacy lookup method. + if game == "Archipelago": + # Keep track of the Archipelago data package separately so if it gets updated in a custom datapackage, + # it updates in all chain maps automatically. + self._archipelago_lookup.clear() + self._archipelago_lookup.update(id_to_name_lookup_table) # defaults starting_reconnect_delay: int = 5 @@ -231,7 +297,7 @@ class CommonContext: # message box reporting a loss of connection _messagebox_connection_loss: typing.Optional["kvui.MessageBox"] = None - def __init__(self, server_address: typing.Optional[str], password: typing.Optional[str]) -> None: + def __init__(self, server_address: typing.Optional[str] = None, password: typing.Optional[str] = None) -> None: # server state self.server_address = server_address self.username = None @@ -271,6 +337,9 @@ def __init__(self, server_address: typing.Optional[str], password: typing.Option self.exit_event = asyncio.Event() self.watcher_event = asyncio.Event() + self.item_names = self.NameLookupDict(self, "item") + self.location_names = self.NameLookupDict(self, "location") + self.jsontotextparser = JSONtoTextParser(self) self.rawjsontotextparser = RawJSONtoTextParser(self) self.update_data_package(network_data_package) @@ -486,19 +555,17 @@ async def prepare_data_package(self, relevant_games: typing.Set[str], or remote_checksum != cache_checksum: needed_updates.add(game) else: - self.update_game(cached_game) + self.update_game(cached_game, game) if needed_updates: await self.send_msgs([{"cmd": "GetDataPackage", "games": [game_name]} for game_name in needed_updates]) - def update_game(self, game_package: dict): - for item_name, item_id in game_package["item_name_to_id"].items(): - self.item_names[item_id] = item_name - for location_name, location_id in game_package["location_name_to_id"].items(): - self.location_names[location_id] = location_name + def update_game(self, game_package: dict, game: str): + self.item_names.update_game(game, game_package["item_name_to_id"]) + self.location_names.update_game(game, game_package["location_name_to_id"]) def update_data_package(self, data_package: dict): for game, game_data in data_package["games"].items(): - self.update_game(game_data) + self.update_game(game_data, game) def consume_network_data_package(self, data_package: dict): self.update_data_package(data_package) diff --git a/MultiServer.py b/MultiServer.py index 4fb03732d811..2c08b0b4eb08 100644 --- a/MultiServer.py +++ b/MultiServer.py @@ -168,9 +168,11 @@ class Context: slot_info: typing.Dict[int, NetworkSlot] generator_version = Version(0, 0, 0) checksums: typing.Dict[str, str] - item_names: typing.Dict[int, str] = Utils.KeyedDefaultDict(lambda code: f'Unknown item (ID:{code})') + item_names: typing.Dict[str, typing.Dict[int, str]] = ( + collections.defaultdict(lambda: Utils.KeyedDefaultDict(lambda code: f'Unknown item (ID:{code})'))) item_name_groups: typing.Dict[str, typing.Dict[str, typing.Set[str]]] - location_names: typing.Dict[int, str] = Utils.KeyedDefaultDict(lambda code: f'Unknown location (ID:{code})') + location_names: typing.Dict[str, typing.Dict[int, str]] = ( + collections.defaultdict(lambda: Utils.KeyedDefaultDict(lambda code: f'Unknown location (ID:{code})'))) location_name_groups: typing.Dict[str, typing.Dict[str, typing.Set[str]]] all_item_and_group_names: typing.Dict[str, typing.Set[str]] all_location_and_group_names: typing.Dict[str, typing.Set[str]] @@ -271,14 +273,21 @@ def _init_game_data(self): if "checksum" in game_package: self.checksums[game_name] = game_package["checksum"] for item_name, item_id in game_package["item_name_to_id"].items(): - self.item_names[item_id] = item_name + self.item_names[game_name][item_id] = item_name for location_name, location_id in game_package["location_name_to_id"].items(): - self.location_names[location_id] = location_name + self.location_names[game_name][location_id] = location_name self.all_item_and_group_names[game_name] = \ set(game_package["item_name_to_id"]) | set(self.item_name_groups[game_name]) self.all_location_and_group_names[game_name] = \ set(game_package["location_name_to_id"]) | set(self.location_name_groups.get(game_name, [])) + archipelago_item_names = self.item_names["Archipelago"] + archipelago_location_names = self.location_names["Archipelago"] + for game in [game_name for game_name in self.gamespackage if game_name != "Archipelago"]: + # Add Archipelago items and locations to each data package. + self.item_names[game].update(archipelago_item_names) + self.location_names[game].update(archipelago_location_names) + def item_names_for_game(self, game: str) -> typing.Optional[typing.Dict[str, int]]: return self.gamespackage[game]["item_name_to_id"] if game in self.gamespackage else None @@ -783,10 +792,7 @@ async def on_client_connected(ctx: Context, client: Client): for slot, connected_clients in clients.items(): if connected_clients: name = ctx.player_names[team, slot] - players.append( - NetworkPlayer(team, slot, - ctx.name_aliases.get((team, slot), name), name) - ) + players.append(NetworkPlayer(team, slot, ctx.name_aliases.get((team, slot), name), name)) games = {ctx.games[x] for x in range(1, len(ctx.games) + 1)} games.add("Archipelago") await ctx.send_msgs(client, [{ @@ -801,8 +807,6 @@ async def on_client_connected(ctx: Context, client: Client): 'permissions': get_permissions(ctx), 'hint_cost': ctx.hint_cost, 'location_check_points': ctx.location_check_points, - 'datapackage_versions': {game: game_data["version"] for game, game_data - in ctx.gamespackage.items() if game in games}, 'datapackage_checksums': {game: game_data["checksum"] for game, game_data in ctx.gamespackage.items() if game in games and "checksum" in game_data}, 'seed_name': ctx.seed_name, @@ -1006,8 +1010,8 @@ def register_location_checks(ctx: Context, team: int, slot: int, locations: typi send_items_to(ctx, team, target_player, new_item) ctx.logger.info('(Team #%d) %s sent %s to %s (%s)' % ( - team + 1, ctx.player_names[(team, slot)], ctx.item_names[item_id], - ctx.player_names[(team, target_player)], ctx.location_names[location])) + team + 1, ctx.player_names[(team, slot)], ctx.item_names[ctx.slot_info[target_player].game][item_id], + ctx.player_names[(team, target_player)], ctx.location_names[ctx.slot_info[slot].game][location])) info_text = json_format_send_event(new_item, target_player) ctx.broadcast_team(team, [info_text]) @@ -1061,8 +1065,8 @@ def collect_hint_location_id(ctx: Context, team: int, slot: int, seeked_location def format_hint(ctx: Context, team: int, hint: NetUtils.Hint) -> str: text = f"[Hint]: {ctx.player_names[team, hint.receiving_player]}'s " \ - f"{ctx.item_names[hint.item]} is " \ - f"at {ctx.location_names[hint.location]} " \ + f"{ctx.item_names[ctx.slot_info[hint.receiving_player].game][hint.item]} is " \ + f"at {ctx.location_names[ctx.slot_info[hint.finding_player].game][hint.location]} " \ f"in {ctx.player_names[team, hint.finding_player]}'s World" if hint.entrance: @@ -1364,7 +1368,7 @@ def _cmd_remaining(self) -> bool: if self.ctx.remaining_mode == "enabled": remaining_item_ids = get_remaining(self.ctx, self.client.team, self.client.slot) if remaining_item_ids: - self.output("Remaining items: " + ", ".join(self.ctx.item_names[item_id] + self.output("Remaining items: " + ", ".join(self.ctx.item_names[self.client.slot.game][item_id] for item_id in remaining_item_ids)) else: self.output("No remaining items found.") @@ -1377,7 +1381,7 @@ def _cmd_remaining(self) -> bool: if self.ctx.client_game_state[self.client.team, self.client.slot] == ClientStatus.CLIENT_GOAL: remaining_item_ids = get_remaining(self.ctx, self.client.team, self.client.slot) if remaining_item_ids: - self.output("Remaining items: " + ", ".join(self.ctx.item_names[item_id] + self.output("Remaining items: " + ", ".join(self.ctx.item_names[self.client.slot.game][item_id] for item_id in remaining_item_ids)) else: self.output("No remaining items found.") @@ -1395,7 +1399,8 @@ def _cmd_missing(self, filter_text="") -> bool: locations = get_missing_checks(self.ctx, self.client.team, self.client.slot) if locations: - names = [self.ctx.location_names[location] for location in locations] + game = self.ctx.slot_info[self.client.slot].game + names = [self.ctx.location_names[game][location] for location in locations] if filter_text: location_groups = self.ctx.location_name_groups[self.ctx.games[self.client.slot]] if filter_text in location_groups: # location group name @@ -1420,7 +1425,8 @@ def _cmd_checked(self, filter_text="") -> bool: locations = get_checked_checks(self.ctx, self.client.team, self.client.slot) if locations: - names = [self.ctx.location_names[location] for location in locations] + game = self.ctx.slot_info[self.client.slot].game + names = [self.ctx.location_names[game][location] for location in locations] if filter_text: location_groups = self.ctx.location_name_groups[self.ctx.games[self.client.slot]] if filter_text in location_groups: # location group name @@ -1501,10 +1507,10 @@ def get_hints(self, input_text: str, for_location: bool = False) -> bool: elif input_text.isnumeric(): game = self.ctx.games[self.client.slot] hint_id = int(input_text) - hint_name = self.ctx.item_names[hint_id] \ - if not for_location and hint_id in self.ctx.item_names \ - else self.ctx.location_names[hint_id] \ - if for_location and hint_id in self.ctx.location_names \ + hint_name = self.ctx.item_names[game][hint_id] \ + if not for_location and hint_id in self.ctx.item_names[game] \ + else self.ctx.location_names[game][hint_id] \ + if for_location and hint_id in self.ctx.location_names[game] \ else None if hint_name in self.ctx.non_hintable_names[game]: self.output(f"Sorry, \"{hint_name}\" is marked as non-hintable.") diff --git a/NetUtils.py b/NetUtils.py index 8fc3929e60b4..076fdc3ba44f 100644 --- a/NetUtils.py +++ b/NetUtils.py @@ -247,7 +247,7 @@ def _handle_item_name(self, node: JSONMessagePart): def _handle_item_id(self, node: JSONMessagePart): item_id = int(node["text"]) - node["text"] = self.ctx.item_names[item_id] + node["text"] = self.ctx.item_names.lookup_in_slot(item_id, node["player"]) return self._handle_item_name(node) def _handle_location_name(self, node: JSONMessagePart): @@ -255,8 +255,8 @@ def _handle_location_name(self, node: JSONMessagePart): return self._handle_color(node) def _handle_location_id(self, node: JSONMessagePart): - item_id = int(node["text"]) - node["text"] = self.ctx.location_names[item_id] + location_id = int(node["text"]) + node["text"] = self.ctx.location_names.lookup_in_slot(location_id, node["player"]) return self._handle_location_name(node) def _handle_entrance_name(self, node: JSONMessagePart): diff --git a/UndertaleClient.py b/UndertaleClient.py index e1538ce81d2e..cdc21c561ab8 100644 --- a/UndertaleClient.py +++ b/UndertaleClient.py @@ -247,8 +247,8 @@ async def process_undertale_cmd(ctx: UndertaleContext, cmd: str, args: dict): with open(os.path.join(ctx.save_game_folder, filename), "w") as f: toDraw = "" for i in range(20): - if i < len(str(ctx.item_names[l.item])): - toDraw += str(ctx.item_names[l.item])[i] + if i < len(str(ctx.item_names.lookup_in_slot(l.item))): + toDraw += str(ctx.item_names.lookup_in_slot(l.item))[i] else: break f.write(toDraw) diff --git a/Utils.py b/Utils.py index 780271996583..9f6837215c5e 100644 --- a/Utils.py +++ b/Utils.py @@ -46,7 +46,7 @@ def as_simple_string(self) -> str: return ".".join(str(item) for item in self) -__version__ = "0.4.6" +__version__ = "0.5.0" version_tuple = tuplize_version(__version__) is_linux = sys.platform.startswith("linux") @@ -458,6 +458,9 @@ class KeyedDefaultDict(collections.defaultdict): """defaultdict variant that uses the missing key as argument to default_factory""" default_factory: typing.Callable[[typing.Any], typing.Any] + def __init__(self, default_factory: typing.Callable[[Any], Any] = None, **kwargs): + super().__init__(default_factory, **kwargs) + def __missing__(self, key): self[key] = value = self.default_factory(key) return value diff --git a/WargrooveClient.py b/WargrooveClient.py index 77180502cefc..c5fdeb3532f5 100644 --- a/WargrooveClient.py +++ b/WargrooveClient.py @@ -176,7 +176,7 @@ def on_package(self, cmd: str, args: dict): if not os.path.isfile(path): open(path, 'w').close() # Announcing commander unlocks - item_name = self.item_names[network_item.item] + item_name = self.item_names.lookup_in_slot(network_item.item) if item_name in faction_table.keys(): for commander in faction_table[item_name]: logger.info(f"{commander.name} has been unlocked!") @@ -197,7 +197,7 @@ def on_package(self, cmd: str, args: dict): open(print_path, 'w').close() with open(print_path, 'w') as f: f.write("Received " + - self.item_names[network_item.item] + + self.item_names.lookup_in_slot(network_item.item) + " from " + self.player_names[network_item.player]) f.close() @@ -342,7 +342,7 @@ def update_commander_data(self): faction_items = 0 faction_item_names = [faction + ' Commanders' for faction in faction_table.keys()] for network_item in self.items_received: - if self.item_names[network_item.item] in faction_item_names: + if self.item_names.lookup_in_slot(network_item.item) in faction_item_names: faction_items += 1 starting_groove = (faction_items - 1) * self.starting_groove_multiplier # Must be an integer larger than 0 diff --git a/WebHostLib/api/__init__.py b/WebHostLib/api/__init__.py index cfdbe25ff2fe..22d1f19f6bdf 100644 --- a/WebHostLib/api/__init__.py +++ b/WebHostLib/api/__init__.py @@ -56,15 +56,6 @@ def get_datapackage(): return network_data_package -@api_endpoints.route('/datapackage_version') -@cache.cached() -def get_datapackage_versions(): - from worlds import AutoWorldRegister - - version_package = {game: world.data_version for game, world in AutoWorldRegister.world_types.items()} - return version_package - - @api_endpoints.route('/datapackage_checksum') @cache.cached() def get_datapackage_checksums(): diff --git a/WebHostLib/templates/ootTracker.html b/WebHostLib/templates/ootTracker.html deleted file mode 100644 index ea7a6d5a4c30..000000000000 --- a/WebHostLib/templates/ootTracker.html +++ /dev/null @@ -1,180 +0,0 @@ - - - - {{ player_name }}'s Tracker - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-
- -
{{ hookshot_length }}
-
-
-
- -
{{ bottle_count if bottle_count > 0 else '' }}
-
-
-
- -
{{ wallet_size }}
-
-
-
- -
Zelda
-
-
-
- -
Epona
-
-
-
- -
Saria
-
-
-
- -
Sun
-
-
-
- -
Time
-
-
-
- -
Storms
-
-
-
- -
{{ token_count }}
-
-
-
- -
Min
-
-
-
- -
Bol
-
-
-
- -
Ser
-
-
-
- -
Req
-
-
-
- -
Noc
-
-
-
- -
Pre
-
-
-
- -
{{ piece_count if piece_count > 0 else '' }}
-
-
- - - - - - - - {% for area in checks_done %} - - - - - - - - {% for location in location_info[area] %} - - - - - - - {% endfor %} - - {% endfor %} -
Items
{{ area }} {{'▼' if area != 'Total'}}{{ small_key_counts.get(area, '-') }}{{ boss_key_counts.get(area, '-') }}{{ checks_done[area] }} / {{ checks_in_area[area] }}
{{ location }}{{ '✔' if location_info[area][location] else '' }}
-
- - diff --git a/Zelda1Client.py b/Zelda1Client.py index cd76a0a5ca78..6d7af0a94dcf 100644 --- a/Zelda1Client.py +++ b/Zelda1Client.py @@ -152,7 +152,7 @@ def get_payload(ctx: ZeldaContext): def reconcile_shops(ctx: ZeldaContext): - checked_location_names = [ctx.location_names[location] for location in ctx.checked_locations] + checked_location_names = [ctx.location_names.lookup_in_slot(location) for location in ctx.checked_locations] shops = [location for location in checked_location_names if "Shop" in location] left_slots = [shop for shop in shops if "Left" in shop] middle_slots = [shop for shop in shops if "Middle" in shop] @@ -190,7 +190,7 @@ async def parse_locations(locations_array, ctx: ZeldaContext, force: bool, zone= locations_checked = [] location = None for location in ctx.missing_locations: - location_name = ctx.location_names[location] + location_name = ctx.location_names.lookup_in_slot(location) if location_name in Locations.overworld_locations and zone == "overworld": status = locations_array[Locations.major_location_offsets[location_name]] diff --git a/docs/network protocol.md b/docs/network protocol.md index 604ff6708fca..da5c41431501 100644 --- a/docs/network protocol.md +++ b/docs/network protocol.md @@ -53,7 +53,7 @@ Example: ``` ## (Server -> Client) -These packets are are sent from the multiworld server to the client. They are not messages which the server accepts. +These packets are sent from the multiworld server to the client. They are not messages which the server accepts. * [RoomInfo](#RoomInfo) * [ConnectionRefused](#ConnectionRefused) * [Connected](#Connected) @@ -80,7 +80,6 @@ Sent to clients when they connect to an Archipelago server. | hint_cost | int | The percentage of total locations that need to be checked to receive a hint from the server. | | location_check_points | int | The amount of hint points you receive per item/location check completed. | | games | list\[str\] | List of games present in this multiworld. | -| datapackage_versions | dict\[str, int\] | Data versions of the individual games' data packages the server will send. Used to decide which games' caches are outdated. See [Data Package Contents](#Data-Package-Contents). **Deprecated. Use `datapackage_checksums` instead.** | | datapackage_checksums | dict[str, str] | Checksum hash of the individual games' data packages the server will send. Used by newer clients to decide which games' caches are outdated. See [Data Package Contents](#Data-Package-Contents) for more information. | | seed_name | str | Uniquely identifying name of this generation | | time | float | Unix time stamp of "now". Send for time synchronization if wanted for things like the DeathLink Bounce. | @@ -500,9 +499,9 @@ In JSON this may look like: {"item": 3, "location": 3, "player": 3, "flags": 0} ] ``` -`item` is the item id of the item. Item ids are in the range of ± 253-1. +`item` is the item id of the item. Item ids are only supported in the range of [-253, 253 - 1], with anything ≤ 0 reserved for Archipelago use. -`location` is the location id of the item inside the world. Location ids are in the range of ± 253-1. +`location` is the location id of the item inside the world. Location ids are only supported in the range of [-253, 253 - 1], with anything ≤ 0 reserved for Archipelago use. `player` is the player slot of the world the item is located in, except when inside an [LocationInfo](#LocationInfo) Packet then it will be the slot of the player to receive the item @@ -646,15 +645,47 @@ class Hint(typing.NamedTuple): ``` ### Data Package Contents -A data package is a JSON object which may contain arbitrary metadata to enable a client to interact with the Archipelago server most easily. Currently, this package is used to send ID to name mappings so that clients need not maintain their own mappings. - -We encourage clients to cache the data package they receive on disk, or otherwise not tied to a session. You will know when your cache is outdated if the [RoomInfo](#RoomInfo) packet or the datapackage itself denote a different version. A special case is datapackage version 0, where it is expected the package is custom and should not be cached. - -Note: - * Any ID is unique to its type across AP: Item 56 only exists once and Location 56 only exists once. - * Any Name is unique to its type across its own Game only: Single Arrow can exist in two games. - * The IDs from the game "Archipelago" may be used in any other game. - Especially Location ID -1: Cheat Console and -2: Server (typically Remote Start Inventory) +A data package is a JSON object which may contain arbitrary metadata to enable a client to interact with the Archipelago +server most easily and not maintain their own mappings. Some contents include: + + - Name to ID mappings for items and locations. + - A checksum of each game's data package for clients to tell if a cached package is invalid. + +We encourage clients to cache the data package they receive on disk, or otherwise not tied to a session. You will know +when your cache is outdated if the [RoomInfo](#RoomInfo) packet or the datapackage itself denote a different checksum +than any locally cached ones. + +**Important Notes about IDs and Names**: + +* IDs ≤ 0 are reserved for "Archipelago" and should not be used by other world implementations. +* The IDs from the game "Archipelago" (in `worlds/generic`) may be used in any world. + * Especially Location ID `-1`: `Cheat Console` and `-2`: `Server` (typically Remote Start Inventory) +* Any names and IDs are only unique in its own world data package, but different games may reuse these names or IDs. + * At runtime, you will need to look up the game of the player to know which item or location ID/Name to lookup in the + data package. This can be easily achieved by reviewing the `slot_info` for a particular player ID prior to lookup. + * For example, a data package like this is valid (Some properties such as `checksum` were omitted): + ```json + { + "games": { + "Game A": { + "location_name_to_id": { + "Boss Chest": 40 + }, + "item_name_to_id": { + "Item X": 12 + } + }, + "Game B": { + "location_name_to_id": { + "Minigame Prize": 40 + }, + "item_name_to_id": { + "Item X": 40 + } + } + } + } + ``` #### Contents | Name | Type | Notes | @@ -668,7 +699,6 @@ GameData is a **dict** but contains these keys and values. It's broken out into |---------------------|----------------|-------------------------------------------------------------------------------------------------------------------------------| | item_name_to_id | dict[str, int] | Mapping of all item names to their respective ID. | | location_name_to_id | dict[str, int] | Mapping of all location names to their respective ID. | -| version | int | Version number of this game's data. Deprecated. Used by older clients to request an updated datapackage if cache is outdated. | | checksum | str | A checksum hash of this game's data. | ### Tags diff --git a/kvui.py b/kvui.py index a1663126cc71..98aa9516b266 100644 --- a/kvui.py +++ b/kvui.py @@ -683,10 +683,18 @@ def refresh_hints(self, hints): for hint in hints: data.append({ "receiving": {"text": self.parser.handle_node({"type": "player_id", "text": hint["receiving_player"]})}, - "item": {"text": self.parser.handle_node( - {"type": "item_id", "text": hint["item"], "flags": hint["item_flags"]})}, + "item": {"text": self.parser.handle_node({ + "type": "item_id", + "text": hint["item"], + "flags": hint["item_flags"], + "player": hint["receiving_player"], + })}, "finding": {"text": self.parser.handle_node({"type": "player_id", "text": hint["finding_player"]})}, - "location": {"text": self.parser.handle_node({"type": "location_id", "text": hint["location"]})}, + "location": {"text": self.parser.handle_node({ + "type": "location_id", + "text": hint["location"], + "player": hint["finding_player"], + })}, "entrance": {"text": self.parser.handle_node({"type": "color" if hint["entrance"] else "text", "color": "blue", "text": hint["entrance"] if hint["entrance"] else "Vanilla"})}, diff --git a/test/general/test_ids.py b/test/general/test_ids.py index 98c41b67b176..e4010af394f5 100644 --- a/test/general/test_ids.py +++ b/test/general/test_ids.py @@ -6,22 +6,6 @@ class TestIDs(unittest.TestCase): - 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 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 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(): diff --git a/test/programs/test_common_client.py b/test/programs/test_common_client.py new file mode 100644 index 000000000000..9936240d17b9 --- /dev/null +++ b/test/programs/test_common_client.py @@ -0,0 +1,106 @@ +import unittest + +import NetUtils +from CommonClient import CommonContext + + +class TestCommonContext(unittest.IsolatedAsyncioTestCase): + async def asyncSetUp(self): + self.ctx = CommonContext() + self.ctx.slot = 1 # Pretend we're player 1 for this. + self.ctx.slot_info.update({ + 1: NetUtils.NetworkSlot("Player 1", "__TestGame1", NetUtils.SlotType.player), + 2: NetUtils.NetworkSlot("Player 2", "__TestGame1", NetUtils.SlotType.player), + 3: NetUtils.NetworkSlot("Player 3", "__TestGame2", NetUtils.SlotType.player), + }) + self.ctx.consume_players_package([ + NetUtils.NetworkPlayer(1, 1, "Player 1", "Player 1"), + NetUtils.NetworkPlayer(1, 2, "Player 2", "Player 2"), + NetUtils.NetworkPlayer(1, 3, "Player 3", "Player 3"), + ]) + # Using IDs outside the "safe range" for testing purposes only. If this fails unit tests, it's because + # another world is not following the spec for allowed ID ranges. + self.ctx.update_data_package({ + "games": { + "__TestGame1": { + "location_name_to_id": { + "Test Location 1 - Safe": 2**54 + 1, + "Test Location 2 - Duplicate": 2**54 + 2, + }, + "item_name_to_id": { + "Test Item 1 - Safe": 2**54 + 1, + "Test Item 2 - Duplicate": 2**54 + 2, + }, + }, + "__TestGame2": { + "location_name_to_id": { + "Test Location 3 - Duplicate": 2**54 + 2, + }, + "item_name_to_id": { + "Test Item 3 - Duplicate": 2**54 + 2, + }, + }, + }, + }) + + async def test_archipelago_datapackage_lookups_exist(self): + assert "Archipelago" in self.ctx.item_names, "Archipelago item names entry does not exist" + assert "Archipelago" in self.ctx.location_names, "Archipelago location names entry does not exist" + + async def test_implicit_name_lookups(self): + # Items + assert self.ctx.item_names[2**54 + 1] == "Test Item 1 - Safe" + assert self.ctx.item_names[2**54 + 3] == f"Unknown item (ID: {2**54+3})" + assert self.ctx.item_names[-1] == "Nothing" + + # Locations + assert self.ctx.location_names[2**54 + 1] == "Test Location 1 - Safe" + assert self.ctx.location_names[2**54 + 3] == f"Unknown location (ID: {2**54+3})" + assert self.ctx.location_names[-1] == "Cheat Console" + + async def test_explicit_name_lookups(self): + # Items + assert self.ctx.item_names["__TestGame1"][2**54+1] == "Test Item 1 - Safe" + assert self.ctx.item_names["__TestGame1"][2**54+2] == "Test Item 2 - Duplicate" + assert self.ctx.item_names["__TestGame1"][2**54+3] == f"Unknown item (ID: {2**54+3})" + assert self.ctx.item_names["__TestGame1"][-1] == "Nothing" + assert self.ctx.item_names["__TestGame2"][2**54+1] == f"Unknown item (ID: {2**54+1})" + assert self.ctx.item_names["__TestGame2"][2**54+2] == "Test Item 3 - Duplicate" + assert self.ctx.item_names["__TestGame2"][2**54+3] == f"Unknown item (ID: {2**54+3})" + assert self.ctx.item_names["__TestGame2"][-1] == "Nothing" + + # Locations + assert self.ctx.location_names["__TestGame1"][2**54+1] == "Test Location 1 - Safe" + assert self.ctx.location_names["__TestGame1"][2**54+2] == "Test Location 2 - Duplicate" + assert self.ctx.location_names["__TestGame1"][2**54+3] == f"Unknown location (ID: {2**54+3})" + assert self.ctx.location_names["__TestGame1"][-1] == "Cheat Console" + assert self.ctx.location_names["__TestGame2"][2**54+1] == f"Unknown location (ID: {2**54+1})" + assert self.ctx.location_names["__TestGame2"][2**54+2] == "Test Location 3 - Duplicate" + assert self.ctx.location_names["__TestGame2"][2**54+3] == f"Unknown location (ID: {2**54+3})" + assert self.ctx.location_names["__TestGame2"][-1] == "Cheat Console" + + async def test_lookup_helper_functions(self): + # Checking own slot. + assert self.ctx.item_names.lookup_in_slot(2 ** 54 + 1) == "Test Item 1 - Safe" + assert self.ctx.item_names.lookup_in_slot(2 ** 54 + 2) == "Test Item 2 - Duplicate" + assert self.ctx.item_names.lookup_in_slot(2 ** 54 + 3) == f"Unknown item (ID: {2 ** 54 + 3})" + assert self.ctx.item_names.lookup_in_slot(-1) == f"Nothing" + + # Checking others' slots. + assert self.ctx.item_names.lookup_in_slot(2 ** 54 + 1, 2) == "Test Item 1 - Safe" + assert self.ctx.item_names.lookup_in_slot(2 ** 54 + 2, 2) == "Test Item 2 - Duplicate" + assert self.ctx.item_names.lookup_in_slot(2 ** 54 + 1, 3) == f"Unknown item (ID: {2 ** 54 + 1})" + assert self.ctx.item_names.lookup_in_slot(2 ** 54 + 2, 3) == "Test Item 3 - Duplicate" + + # Checking by game. + assert self.ctx.item_names.lookup_in_game(2 ** 54 + 1, "__TestGame1") == "Test Item 1 - Safe" + assert self.ctx.item_names.lookup_in_game(2 ** 54 + 2, "__TestGame1") == "Test Item 2 - Duplicate" + assert self.ctx.item_names.lookup_in_game(2 ** 54 + 3, "__TestGame1") == f"Unknown item (ID: {2 ** 54 + 3})" + assert self.ctx.item_names.lookup_in_game(2 ** 54 + 1, "__TestGame2") == f"Unknown item (ID: {2 ** 54 + 1})" + assert self.ctx.item_names.lookup_in_game(2 ** 54 + 2, "__TestGame2") == "Test Item 3 - Duplicate" + + # Checking with Archipelago ids are valid in any game package. + assert self.ctx.item_names.lookup_in_slot(-1, 2) == "Nothing" + assert self.ctx.item_names.lookup_in_slot(-1, 3) == "Nothing" + assert self.ctx.item_names.lookup_in_game(-1, "__TestGame1") == "Nothing" + assert self.ctx.item_names.lookup_in_game(-1, "__TestGame2") == "Nothing" diff --git a/worlds/AutoWorld.py b/worlds/AutoWorld.py index 5d674c0c22fd..6e17f023f6fb 100644 --- a/worlds/AutoWorld.py +++ b/worlds/AutoWorld.py @@ -258,18 +258,6 @@ class World(metaclass=AutoWorldRegister): location_name_groups: ClassVar[Dict[str, Set[str]]] = {} """maps location group names to sets of locations. Example: {"Sewer": {"Sewer Key Drop 1", "Sewer Key Drop 2"}}""" - data_version: ClassVar[int] = 0 - """ - Increment this every time something in your world's names/id mappings changes. - - When this is set to 0, that world's DataPackage is considered in "testing mode", which signals to servers/clients - that it should not be cached, and clients should request that world's DataPackage every connection. Not - recommended for production-ready worlds. - - Deprecated. Clients should utilize `checksum` to determine if DataPackage has changed since last connection and - request a new DataPackage, if necessary. - """ - required_client_version: Tuple[int, int, int] = (0, 1, 6) """ override this if changes to a world break forward-compatibility of the client @@ -543,7 +531,6 @@ def get_data_package_data(cls) -> "GamesPackage": "item_name_to_id": cls.item_name_to_id, "location_name_groups": sorted_location_name_groups, "location_name_to_id": cls.location_name_to_id, - "version": cls.data_version, } res["checksum"] = data_package_checksum(res) return res diff --git a/worlds/__init__.py b/worlds/__init__.py index 53b0c5ceb948..09f72882195e 100644 --- a/worlds/__init__.py +++ b/worlds/__init__.py @@ -33,7 +33,6 @@ class GamesPackage(TypedDict, total=False): location_name_groups: Dict[str, List[str]] location_name_to_id: Dict[str, int] checksum: str - version: int # TODO: Remove support after per game data packages API change. class DataPackage(TypedDict): diff --git a/worlds/adventure/__init__.py b/worlds/adventure/__init__.py index 84caca828f2c..1c2583b3ed6e 100644 --- a/worlds/adventure/__init__.py +++ b/worlds/adventure/__init__.py @@ -113,7 +113,6 @@ class AdventureWorld(World): settings: ClassVar[AdventureSettings] item_name_to_id: ClassVar[Dict[str, int]] = {name: data.id for name, data in item_table.items()} location_name_to_id: ClassVar[Dict[str, int]] = {name: data.location_id for name, data in location_table.items()} - data_version: ClassVar[int] = 1 required_client_version: Tuple[int, int, int] = (0, 3, 9) def __init__(self, world: MultiWorld, player: int): diff --git a/worlds/alttp/Client.py b/worlds/alttp/Client.py index 5b27f559efd7..db7555f24615 100644 --- a/worlds/alttp/Client.py +++ b/worlds/alttp/Client.py @@ -339,7 +339,7 @@ async def track_locations(ctx, roomid, roomdata) -> bool: def new_check(location_id): new_locations.append(location_id) ctx.locations_checked.add(location_id) - location = ctx.location_names[location_id] + location = ctx.location_names.lookup_in_slot(location_id) snes_logger.info( f'New Check: {location} ' + f'({len(ctx.checked_locations) + 1 if ctx.checked_locations else len(ctx.locations_checked)}/' + @@ -552,9 +552,9 @@ async def game_watcher(self, ctx): item = ctx.items_received[recv_index] recv_index += 1 logging.info('Received %s from %s (%s) (%d/%d in list)' % ( - color(ctx.item_names[item.item], 'red', 'bold'), + color(ctx.item_names.lookup_in_slot(item.item), 'red', 'bold'), color(ctx.player_names[item.player], 'yellow'), - ctx.location_names[item.location], recv_index, len(ctx.items_received))) + ctx.location_names.lookup_in_slot(item.location, item.player), recv_index, len(ctx.items_received))) snes_buffered_write(ctx, RECV_PROGRESS_ADDR, bytes([recv_index & 0xFF, (recv_index >> 8) & 0xFF])) diff --git a/worlds/alttp/__init__.py b/worlds/alttp/__init__.py index ae3dfe9e3b1a..3176f7a7fcce 100644 --- a/worlds/alttp/__init__.py +++ b/worlds/alttp/__init__.py @@ -213,7 +213,6 @@ class ALTTPWorld(World): item_name_to_id = {name: data.item_code for name, data in item_table.items() if type(data.item_code) == int} location_name_to_id = lookup_name_to_id - data_version = 9 required_client_version = (0, 4, 1) web = ALTTPWeb() diff --git a/worlds/bk_sudoku/__init__.py b/worlds/bk_sudoku/__init__.py index 195339c38070..2c57bc7301ff 100644 --- a/worlds/bk_sudoku/__init__.py +++ b/worlds/bk_sudoku/__init__.py @@ -34,7 +34,6 @@ class Bk_SudokuWorld(World): """ game = "Sudoku" web = Bk_SudokuWebWorld() - data_version = 1 item_name_to_id: Dict[str, int] = {} location_name_to_id: Dict[str, int] = {} diff --git a/worlds/blasphemous/__init__.py b/worlds/blasphemous/__init__.py index 9abcd81b20e1..a46fb55b9541 100644 --- a/worlds/blasphemous/__init__.py +++ b/worlds/blasphemous/__init__.py @@ -32,7 +32,6 @@ class BlasphemousWorld(World): game: str = "Blasphemous" web = BlasphemousWeb() - data_version = 2 item_name_to_id = {item["name"]: (base_id + index) for index, item in enumerate(item_table)} location_name_to_id = {loc["name"]: (base_id + index) for index, loc in enumerate(location_table)} diff --git a/worlds/bumpstik/__init__.py b/worlds/bumpstik/__init__.py index d922c0277ac3..fe261dc94d30 100644 --- a/worlds/bumpstik/__init__.py +++ b/worlds/bumpstik/__init__.py @@ -39,8 +39,6 @@ class BumpStikWorld(World): location_name_to_id = location_table item_name_groups = item_groups - data_version = 1 - required_client_version = (0, 3, 8) options: BumpstikOptions diff --git a/worlds/checksfinder/__init__.py b/worlds/checksfinder/__init__.py index b70c65bb08f5..c8b9587f8500 100644 --- a/worlds/checksfinder/__init__.py +++ b/worlds/checksfinder/__init__.py @@ -33,8 +33,6 @@ class ChecksFinderWorld(World): item_name_to_id = {name: data.code for name, data in item_table.items()} location_name_to_id = {name: data.id for name, data in advancement_table.items()} - data_version = 4 - def _get_checksfinder_data(self): return { 'world_seed': self.multiworld.per_slot_randoms[self.player].getrandbits(32), diff --git a/worlds/clique/__init__.py b/worlds/clique/__init__.py index 30c0e47f818e..b5cc74d94ac0 100644 --- a/worlds/clique/__init__.py +++ b/worlds/clique/__init__.py @@ -37,7 +37,6 @@ class CliqueWorld(World): """The greatest game of all time.""" game = "Clique" - data_version = 3 web = CliqueWebWorld() option_definitions = clique_options location_name_to_id = location_table diff --git a/worlds/cv64/__init__.py b/worlds/cv64/__init__.py index 2f483cd4d919..0d384acc8f3d 100644 --- a/worlds/cv64/__init__.py +++ b/worlds/cv64/__init__.py @@ -64,7 +64,6 @@ class CV64World(World): options: CV64Options settings: typing.ClassVar[CV64Settings] topology_present = True - data_version = 1 item_name_to_id = get_item_names_to_ids() location_name_to_id = get_location_names_to_ids() diff --git a/worlds/cv64/client.py b/worlds/cv64/client.py index ff9c79f578be..bea8ce38825d 100644 --- a/worlds/cv64/client.py +++ b/worlds/cv64/client.py @@ -146,7 +146,7 @@ async def game_watcher(self, ctx: "BizHawkClientContext") -> None: text_color = bytearray([0xA2, 0x0B]) else: text_color = bytearray([0xA2, 0x02]) - received_text, num_lines = cv64_text_wrap(f"{ctx.item_names[next_item.item]}\n" + received_text, num_lines = cv64_text_wrap(f"{ctx.item_names.lookup_in_slot(next_item.item)}\n" f"from {ctx.player_names[next_item.player]}", 96) await bizhawk.guarded_write(ctx.bizhawk_ctx, [(0x389BE1, [next_item.item & 0xFF], "RDRAM"), diff --git a/worlds/dark_souls_3/__init__.py b/worlds/dark_souls_3/__init__.py index c4b2232b32dc..020010981160 100644 --- a/worlds/dark_souls_3/__init__.py +++ b/worlds/dark_souls_3/__init__.py @@ -49,7 +49,6 @@ class DarkSouls3World(World): option_definitions = dark_souls_options topology_present: bool = True web = DarkSouls3Web() - data_version = 8 base_id = 100000 enabled_location_categories: Set[DS3LocationCategory] required_client_version = (0, 4, 2) diff --git a/worlds/dkc3/Client.py b/worlds/dkc3/Client.py index efa199e1d0c9..8e4a1bf2a423 100644 --- a/worlds/dkc3/Client.py +++ b/worlds/dkc3/Client.py @@ -86,7 +86,7 @@ async def game_watcher(self, ctx): for new_check_id in new_checks: ctx.locations_checked.add(new_check_id) - location = ctx.location_names[new_check_id] + location = ctx.location_names.lookup_in_slot(new_check_id) snes_logger.info( f'New Check: {location} ({len(ctx.locations_checked)}/{len(ctx.missing_locations) + len(ctx.checked_locations)})') await ctx.send_msgs([{"cmd": 'LocationChecks', "locations": [new_check_id]}]) @@ -99,9 +99,9 @@ async def game_watcher(self, ctx): item = ctx.items_received[recv_index] recv_index += 1 logging.info('Received %s from %s (%s) (%d/%d in list)' % ( - color(ctx.item_names[item.item], 'red', 'bold'), + color(ctx.item_names.lookup_in_slot(item.item), 'red', 'bold'), color(ctx.player_names[item.player], 'yellow'), - ctx.location_names[item.location], recv_index, len(ctx.items_received))) + ctx.location_names.lookup_in_slot(item.location, item.player), recv_index, len(ctx.items_received))) snes_buffered_write(ctx, DKC3_RECV_PROGRESS_ADDR, bytes([recv_index])) if item.item in item_rom_data: diff --git a/worlds/dkc3/__init__.py b/worlds/dkc3/__init__.py index f298114905fd..de6fb4a44a03 100644 --- a/worlds/dkc3/__init__.py +++ b/worlds/dkc3/__init__.py @@ -61,7 +61,6 @@ class DKC3World(World): options: DKC3Options topology_present = False - data_version = 2 #hint_blacklist = {LocationName.rocket_rush_flag} item_name_to_id = {name: data.code for name, data in item_table.items()} diff --git a/worlds/dlcquest/__init__.py b/worlds/dlcquest/__init__.py index ca2862113fd4..a9dfcc5044b1 100644 --- a/worlds/dlcquest/__init__.py +++ b/worlds/dlcquest/__init__.py @@ -43,8 +43,6 @@ class DLCqworld(World): item_name_to_id = {name: data.code for name, data in item_table.items()} location_name_to_id = location_table - data_version = 1 - options_dataclass = DLCQuestOptions options: DLCQuestOptions diff --git a/worlds/doom_1993/__init__.py b/worlds/doom_1993/__init__.py index ace33f994c33..b6138ae07103 100644 --- a/worlds/doom_1993/__init__.py +++ b/worlds/doom_1993/__init__.py @@ -42,7 +42,6 @@ class DOOM1993World(World): options: DOOM1993Options game = "DOOM 1993" web = DOOM1993Web() - data_version = 3 required_client_version = (0, 3, 9) item_name_to_id = {data["name"]: item_id for item_id, data in Items.item_table.items()} diff --git a/worlds/doom_ii/__init__.py b/worlds/doom_ii/__init__.py index daad94553517..38840f552a13 100644 --- a/worlds/doom_ii/__init__.py +++ b/worlds/doom_ii/__init__.py @@ -43,7 +43,6 @@ class DOOM2World(World): options: DOOM2Options game = "DOOM II" web = DOOM2Web() - data_version = 3 required_client_version = (0, 3, 9) item_name_to_id = {data["name"]: item_id for item_id, data in Items.item_table.items()} diff --git a/worlds/factorio/Client.py b/worlds/factorio/Client.py index d245e1bb7af6..258a5445328c 100644 --- a/worlds/factorio/Client.py +++ b/worlds/factorio/Client.py @@ -247,7 +247,7 @@ async def game_watcher(ctx: FactorioContext): if ctx.locations_checked != research_data: bridge_logger.debug( f"New researches done: " - f"{[ctx.location_names[rid] for rid in research_data - ctx.locations_checked]}") + f"{[ctx.location_names.lookup_in_slot(rid) for rid in research_data - ctx.locations_checked]}") ctx.locations_checked = research_data await ctx.send_msgs([{"cmd": 'LocationChecks', "locations": tuple(research_data)}]) death_link_tick = data.get("death_link_tick", 0) @@ -360,7 +360,7 @@ async def factorio_server_watcher(ctx: FactorioContext): transfer_item: NetworkItem = ctx.items_received[ctx.send_index] item_id = transfer_item.item player_name = ctx.player_names[transfer_item.player] - item_name = ctx.item_names[item_id] + item_name = ctx.item_names.lookup_in_slot(item_id) factorio_server_logger.info(f"Sending {item_name} to Nauvis from {player_name}.") commands[ctx.send_index] = f"/ap-get-technology {item_name}\t{ctx.send_index}\t{player_name}" ctx.send_index += 1 diff --git a/worlds/factorio/__init__.py b/worlds/factorio/__init__.py index 3b7475738489..1ea2f6e4c98c 100644 --- a/worlds/factorio/__init__.py +++ b/worlds/factorio/__init__.py @@ -95,7 +95,6 @@ class Factorio(World): item_name_groups = { "Progressive": set(progressive_tech_table.keys()), } - data_version = 8 required_client_version = (0, 4, 2) ordered_science_packs: typing.List[str] = MaxSciencePack.get_ordered_science_packs() diff --git a/worlds/ff1/__init__.py b/worlds/ff1/__init__.py index ce5519b13a1a..3a5047506850 100644 --- a/worlds/ff1/__init__.py +++ b/worlds/ff1/__init__.py @@ -40,7 +40,6 @@ class FF1World(World): settings_key = "ffr_options" game = "Final Fantasy" topology_present = False - data_version = 2 ff1_items = FF1Items() ff1_locations = FF1Locations() diff --git a/worlds/ffmq/__init__.py b/worlds/ffmq/__init__.py index b995cc427c9b..ac3e91370933 100644 --- a/worlds/ffmq/__init__.py +++ b/worlds/ffmq/__init__.py @@ -56,8 +56,6 @@ class FFMQWorld(World): create_regions = create_regions set_rules = set_rules stage_set_rules = stage_set_rules - - data_version = 1 web = FFMQWebWorld() # settings: FFMQSettings @@ -216,4 +214,3 @@ def extend_hint_information(self, hint_data): hint_data[self.player][location.address] += f"/{hint}" else: hint_data[self.player][location.address] = hint - diff --git a/worlds/generic/__init__.py b/worlds/generic/__init__.py index 6b2ffdfee180..b88295b43237 100644 --- a/worlds/generic/__init__.py +++ b/worlds/generic/__init__.py @@ -40,7 +40,6 @@ class GenericWorld(World): } hidden = True web = GenericWeb() - data_version = 1 def generate_early(self): self.multiworld.player_types[self.player] = SlotType.spectator # mark as spectator diff --git a/worlds/heretic/__init__.py b/worlds/heretic/__init__.py index c83cdb9477b2..fc5ffdd2de2b 100644 --- a/worlds/heretic/__init__.py +++ b/worlds/heretic/__init__.py @@ -41,7 +41,6 @@ class HereticWorld(World): options: HereticOptions game = "Heretic" web = HereticWeb() - data_version = 3 required_client_version = (0, 3, 9) item_name_to_id = {data["name"]: item_id for item_id, data in Items.item_table.items()} diff --git a/worlds/hk/__init__.py b/worlds/hk/__init__.py index 3530030fa695..fdaece8d34cd 100644 --- a/worlds/hk/__init__.py +++ b/worlds/hk/__init__.py @@ -154,7 +154,6 @@ class HKWorld(World): ranges: typing.Dict[str, typing.Tuple[int, int]] charm_costs: typing.List[int] cached_filler_items = {} - data_version = 2 def __init__(self, world, player): super(HKWorld, self).__init__(world, player) diff --git a/worlds/hylics2/__init__.py b/worlds/hylics2/__init__.py index be7ebf199127..18bcb0edc143 100644 --- a/worlds/hylics2/__init__.py +++ b/worlds/hylics2/__init__.py @@ -37,8 +37,6 @@ class Hylics2World(World): options_dataclass = Hylics2Options options: Hylics2Options - data_version = 3 - def set_rules(self): Rules.set_rules(self) diff --git a/worlds/kdl3/Client.py b/worlds/kdl3/Client.py index e33a680bc025..6faa8206c2df 100644 --- a/worlds/kdl3/Client.py +++ b/worlds/kdl3/Client.py @@ -330,9 +330,9 @@ async def game_watcher(self, ctx) -> None: item = ctx.items_received[recv_amount] recv_amount += 1 logging.info('Received %s from %s (%s) (%d/%d in list)' % ( - color(ctx.item_names[item.item], 'red', 'bold'), + color(ctx.item_names.lookup_in_slot(item.item), 'red', 'bold'), color(ctx.player_names[item.player], 'yellow'), - ctx.location_names[item.location], recv_amount, len(ctx.items_received))) + ctx.location_names.lookup_in_slot(item.location, item.player), recv_amount, len(ctx.items_received))) snes_buffered_write(ctx, KDL3_RECV_COUNT, pack("H", recv_amount)) item_idx = item.item & 0x00000F @@ -415,7 +415,7 @@ async def game_watcher(self, ctx) -> None: for new_check_id in new_checks: ctx.locations_checked.add(new_check_id) - location = ctx.location_names[new_check_id] + location = ctx.location_names.lookup_in_slot(new_check_id) snes_logger.info( f'New Check: {location} ({len(ctx.locations_checked)}/' f'{len(ctx.missing_locations) + len(ctx.checked_locations)})') diff --git a/worlds/ladx/__init__.py b/worlds/ladx/__init__.py index 6c7517f359dc..f7de0f41f9c0 100644 --- a/worlds/ladx/__init__.py +++ b/worlds/ladx/__init__.py @@ -78,11 +78,6 @@ class LinksAwakeningWorld(World): settings: typing.ClassVar[LinksAwakeningSettings] topology_present = True # show path to required location checks in spoiler - # data_version is used to signal that items, locations or their names - # changed. Set this to 0 during development so other games' clients do not - # cache any texts, then increase by 1 for each release that makes changes. - data_version = 1 - # ID of first item and location, could be hard-coded but code may be easier # to read with this as a propery. base_id = BASE_ID diff --git a/worlds/lingo/__init__.py b/worlds/lingo/__init__.py index fa24fdc3bc63..302e7e1d85b2 100644 --- a/worlds/lingo/__init__.py +++ b/worlds/lingo/__init__.py @@ -37,7 +37,6 @@ class LingoWorld(World): base_id = 444400 topology_present = True - data_version = 1 options_dataclass = LingoOptions options: LingoOptions diff --git a/worlds/lufia2ac/Client.py b/worlds/lufia2ac/Client.py index 3c05e6395d90..1e8437d20e84 100644 --- a/worlds/lufia2ac/Client.py +++ b/worlds/lufia2ac/Client.py @@ -147,9 +147,9 @@ async def game_watcher(self, ctx: SNIContext) -> None: snes_items_received += 1 snes_logger.info("Received %s from %s (%s) (%d/%d in list)" % ( - ctx.item_names[item.item], + ctx.item_names.lookup_in_slot(item.item), ctx.player_names[item.player], - ctx.location_names[item.location], + ctx.location_names.lookup_in_slot(item.location, item.player), snes_items_received, len(ctx.items_received))) snes_buffered_write(ctx, L2AC_RX_ADDR + 2 * (snes_items_received + 1), item_code.to_bytes(2, "little")) snes_buffered_write(ctx, L2AC_RX_ADDR, snes_items_received.to_bytes(2, "little")) diff --git a/worlds/lufia2ac/__init__.py b/worlds/lufia2ac/__init__.py index 561429c825f3..6433452cefea 100644 --- a/worlds/lufia2ac/__init__.py +++ b/worlds/lufia2ac/__init__.py @@ -65,7 +65,6 @@ class L2ACWorld(World): "Iris treasures": {name for name, data in l2ac_item_table.items() if data.type is ItemType.IRIS_TREASURE}, "Party members": {name for name, data in l2ac_item_table.items() if data.type is ItemType.PARTY_MEMBER}, } - data_version: ClassVar[int] = 2 required_client_version: Tuple[int, int, int] = (0, 4, 4) # L2ACWorld specific properties diff --git a/worlds/meritous/__init__.py b/worlds/meritous/__init__.py index 728d7af8616d..7a21b19ef247 100644 --- a/worlds/meritous/__init__.py +++ b/worlds/meritous/__init__.py @@ -44,8 +44,6 @@ class MeritousWorld(World): location_name_to_id = location_table item_name_groups = item_groups - data_version = 2 - # NOTE: Remember to change this before this game goes live required_client_version = (0, 2, 4) diff --git a/worlds/minecraft/__init__.py b/worlds/minecraft/__init__.py index 343b9bad19a9..75e043d0cbaf 100644 --- a/worlds/minecraft/__init__.py +++ b/worlds/minecraft/__init__.py @@ -92,8 +92,6 @@ class MinecraftWorld(World): item_name_to_id = Constants.item_name_to_id location_name_to_id = Constants.location_name_to_id - data_version = 7 - def _get_mc_data(self) -> Dict[str, Any]: exits = [connection[0] for connection in Constants.region_info["default_connections"]] return { diff --git a/worlds/mmbn3/__init__.py b/worlds/mmbn3/__init__.py index eac8a37bf06d..97725e728bae 100644 --- a/worlds/mmbn3/__init__.py +++ b/worlds/mmbn3/__init__.py @@ -57,8 +57,6 @@ class MMBN3World(World): settings: typing.ClassVar[MMBN3Settings] topology_present = False - data_version = 1 - item_name_to_id = {name: data.code for name, data in item_table.items()} location_name_to_id = {loc_data.name: loc_data.id for loc_data in all_locations} diff --git a/worlds/noita/__init__.py b/worlds/noita/__init__.py index 43078c5e4320..af2921768d6a 100644 --- a/worlds/noita/__init__.py +++ b/worlds/noita/__init__.py @@ -34,14 +34,13 @@ class NoitaWorld(World): item_name_groups = items.item_name_groups location_name_groups = locations.location_name_groups - data_version = 2 web = NoitaWeb() def generate_early(self) -> None: if not self.multiworld.get_player_name(self.player).isascii(): raise Exception("Noita yaml's slot name has invalid character(s).") - + # Returned items will be sent over to the client def fill_slot_data(self) -> Dict[str, Any]: return self.options.as_dict("death_link", "victory_condition", "path_option", "hidden_chests", diff --git a/worlds/oot/__init__.py b/worlds/oot/__init__.py index d9ee63850eaf..9346ac55103a 100644 --- a/worlds/oot/__init__.py +++ b/worlds/oot/__init__.py @@ -150,8 +150,6 @@ class OOTWorld(World): location_name_to_id = location_name_to_id web = OOTWeb() - data_version = 3 - required_client_version = (0, 4, 0) item_name_groups = { diff --git a/worlds/overcooked2/__init__.py b/worlds/overcooked2/__init__.py index be66fa3a8a1e..44227d4becaa 100644 --- a/worlds/overcooked2/__init__.py +++ b/worlds/overcooked2/__init__.py @@ -48,7 +48,6 @@ class Overcooked2World(World): web = Overcooked2Web() required_client_version = (0, 3, 8) topology_present: bool = False - data_version = 3 item_name_to_id = item_name_to_id item_id_to_name = item_id_to_name diff --git a/worlds/pokemon_emerald/__init__.py b/worlds/pokemon_emerald/__init__.py index 6225350a5e51..3e50f748d922 100644 --- a/worlds/pokemon_emerald/__init__.py +++ b/worlds/pokemon_emerald/__init__.py @@ -87,7 +87,6 @@ class PokemonEmeraldWorld(World): item_name_groups = ITEM_GROUPS location_name_groups = LOCATION_GROUPS - data_version = 2 required_client_version = (0, 4, 6) badge_shuffle_info: Optional[List[Tuple[PokemonEmeraldLocation, PokemonEmeraldItem]]] diff --git a/worlds/pokemon_rb/__init__.py b/worlds/pokemon_rb/__init__.py index 79028a68b187..003e0a32e97d 100644 --- a/worlds/pokemon_rb/__init__.py +++ b/worlds/pokemon_rb/__init__.py @@ -74,7 +74,6 @@ class PokemonRedBlueWorld(World): option_definitions = pokemon_rb_options settings: typing.ClassVar[PokemonSettings] - data_version = 9 required_client_version = (0, 4, 2) topology_present = True diff --git a/worlds/raft/__init__.py b/worlds/raft/__init__.py index 8e4eda09e10f..e96cd4471268 100644 --- a/worlds/raft/__init__.py +++ b/worlds/raft/__init__.py @@ -39,7 +39,6 @@ class RaftWorld(World): location_name_to_id = locations_lookup_name_to_id option_definitions = raft_options - data_version = 2 required_client_version = (0, 3, 4) def create_items(self): diff --git a/worlds/rogue_legacy/__init__.py b/worlds/rogue_legacy/__init__.py index c5a8d71b5d63..eb657699540f 100644 --- a/worlds/rogue_legacy/__init__.py +++ b/worlds/rogue_legacy/__init__.py @@ -35,7 +35,6 @@ class RLWorld(World): game = "Rogue Legacy" option_definitions = rl_options topology_present = True - data_version = 4 required_client_version = (0, 3, 5) web = RLWeb() diff --git a/worlds/ror2/__init__.py b/worlds/ror2/__init__.py index 5afdb797e7de..b6a1901a8db1 100644 --- a/worlds/ror2/__init__.py +++ b/worlds/ror2/__init__.py @@ -44,7 +44,6 @@ class RiskOfRainWorld(World): } location_name_to_id = item_pickups - data_version = 9 required_client_version = (0, 4, 5) web = RiskOfWeb() total_revivals: int diff --git a/worlds/sa2b/__init__.py b/worlds/sa2b/__init__.py index 6279aa943263..f7d1ca72b09f 100644 --- a/worlds/sa2b/__init__.py +++ b/worlds/sa2b/__init__.py @@ -58,7 +58,6 @@ class SA2BWorld(World): options_dataclass = SA2BOptions options: SA2BOptions topology_present = False - data_version = 7 item_name_groups = item_groups item_name_to_id = {name: data.code for name, data in item_table.items()} diff --git a/worlds/sc2/Client.py b/worlds/sc2/Client.py index 4e55509dda48..ac9ccfffcdbc 100644 --- a/worlds/sc2/Client.py +++ b/worlds/sc2/Client.py @@ -244,10 +244,10 @@ def print_faction_title(): self.formatted_print(f" [u]{faction.name}[/u] ") for item_id in categorized_items[faction]: - item_name = self.ctx.item_names[item_id] + item_name = self.ctx.item_names.lookup_in_slot(item_id) received_child_items = items_received_set.intersection(parent_to_child.get(item_id, [])) matching_children = [child for child in received_child_items - if item_matches_filter(self.ctx.item_names[child])] + if item_matches_filter(self.ctx.item_names.lookup_in_slot(child))] received_items_of_this_type = items_received.get(item_id, []) item_is_match = item_matches_filter(item_name) if item_is_match or len(matching_children) > 0: @@ -1165,7 +1165,7 @@ def request_unfinished_missions(ctx: SC2Context) -> None: objectives = set(ctx.locations_for_mission(mission)) if objectives: remaining_objectives = objectives.difference(ctx.checked_locations) - unfinished_locations[mission] = [ctx.location_names[location_id] for location_id in remaining_objectives] + unfinished_locations[mission] = [ctx.location_names.lookup_in_slot(location_id) for location_id in remaining_objectives] else: unfinished_locations[mission] = [] diff --git a/worlds/sc2/ClientGui.py b/worlds/sc2/ClientGui.py index 167583fd1ecb..f9dcfc18eb4a 100644 --- a/worlds/sc2/ClientGui.py +++ b/worlds/sc2/ClientGui.py @@ -269,7 +269,7 @@ def sort_unfinished_locations(self, mission_name: str) -> Tuple[Dict[LocationTyp for loc in self.ctx.locations_for_mission(mission_name): if loc in self.ctx.missing_locations: count += 1 - locations[lookup_location_id_to_type[loc]].append(self.ctx.location_names[loc]) + locations[lookup_location_id_to_type[loc]].append(self.ctx.location_names.lookup_in_slot(loc)) plando_locations = [] for plando_loc in self.ctx.plando_locations: diff --git a/worlds/shorthike/__init__.py b/worlds/shorthike/__init__.py index 3e0430f024ca..470b061c4bc0 100644 --- a/worlds/shorthike/__init__.py +++ b/worlds/shorthike/__init__.py @@ -28,7 +28,6 @@ class ShortHikeWorld(World): game = "A Short Hike" web = ShortHikeWeb() - data_version = 2 item_name_to_id = {item["name"]: item["id"] for item in item_table} location_name_to_id = {loc["name"]: loc["id"] for loc in location_table} diff --git a/worlds/sm/Client.py b/worlds/sm/Client.py index 7c97f743c552..6d6dd08ba5df 100644 --- a/worlds/sm/Client.py +++ b/worlds/sm/Client.py @@ -123,7 +123,7 @@ async def game_watcher(self, ctx): location_id = locations_start_id + item_index ctx.locations_checked.add(location_id) - location = ctx.location_names[location_id] + location = ctx.location_names.lookup_in_slot(location_id) snes_logger.info( f'New Check: {location} ({len(ctx.locations_checked)}/{len(ctx.missing_locations) + len(ctx.checked_locations)})') await ctx.send_msgs([{"cmd": 'LocationChecks', "locations": [location_id]}]) @@ -151,9 +151,8 @@ async def game_watcher(self, ctx): snes_buffered_write(ctx, SM_RECV_QUEUE_WCOUNT, bytes([item_out_ptr & 0xFF, (item_out_ptr >> 8) & 0xFF])) logging.info('Received %s from %s (%s) (%d/%d in list)' % ( - color(ctx.item_names[item.item], 'red', 'bold'), + color(ctx.item_names.lookup_in_slot(item.item), 'red', 'bold'), color(ctx.player_names[item.player], 'yellow'), - ctx.location_names[item.location], item_out_ptr, len(ctx.items_received))) + ctx.location_names.lookup_in_slot(item.location, item.player), item_out_ptr, len(ctx.items_received))) await snes_flush_writes(ctx) - diff --git a/worlds/sm/__init__.py b/worlds/sm/__init__.py index 7f12bf484c0f..826b1447793d 100644 --- a/worlds/sm/__init__.py +++ b/worlds/sm/__init__.py @@ -99,7 +99,6 @@ class SMWorld(World): game: str = "Super Metroid" topology_present = True - data_version = 3 option_definitions = sm_options settings: typing.ClassVar[SMSettings] diff --git a/worlds/sm64ex/__init__.py b/worlds/sm64ex/__init__.py index 0e944aa4ab4b..833ae56ca302 100644 --- a/worlds/sm64ex/__init__.py +++ b/worlds/sm64ex/__init__.py @@ -35,7 +35,6 @@ class SM64World(World): item_name_to_id = item_table location_name_to_id = location_table - data_version = 9 required_client_version = (0, 3, 5) area_connections: typing.Dict[int, int] diff --git a/worlds/smw/Client.py b/worlds/smw/Client.py index 33a74b3dc80f..85bb3fe1ee16 100644 --- a/worlds/smw/Client.py +++ b/worlds/smw/Client.py @@ -448,7 +448,7 @@ async def game_watcher(self, ctx): for new_check_id in new_checks: ctx.locations_checked.add(new_check_id) - location = ctx.location_names[new_check_id] + location = ctx.location_names.lookup_in_slot(new_check_id) snes_logger.info( f'New Check: {location} ({len(ctx.locations_checked)}/{len(ctx.missing_locations) + len(ctx.checked_locations)})') await ctx.send_msgs([{"cmd": 'LocationChecks', "locations": [new_check_id]}]) @@ -499,15 +499,16 @@ async def game_watcher(self, ctx): if recv_index < len(ctx.items_received): item = ctx.items_received[recv_index] recv_index += 1 + sending_game = ctx.slot_info[item.player].game logging.info('Received %s from %s (%s) (%d/%d in list)' % ( - color(ctx.item_names[item.item], 'red', 'bold'), + color(ctx.item_names.lookup_in_slot(item.item), 'red', 'bold'), color(ctx.player_names[item.player], 'yellow'), - ctx.location_names[item.location], recv_index, len(ctx.items_received))) + ctx.location_names.lookup_in_slot(item.location, item.player), recv_index, len(ctx.items_received))) if self.should_show_message(ctx, item): if item.item != 0xBC0012 and item.item not in trap_rom_data: # Don't send messages for Boss Tokens - item_name = ctx.item_names[item.item] + item_name = ctx.item_names.lookup_in_slot(item.item) player_name = ctx.player_names[item.player] receive_message = generate_received_text(item_name, player_name) @@ -515,7 +516,7 @@ async def game_watcher(self, ctx): snes_buffered_write(ctx, SMW_RECV_PROGRESS_ADDR, bytes([recv_index&0xFF, (recv_index>>8)&0xFF])) if item.item in trap_rom_data: - item_name = ctx.item_names[item.item] + item_name = ctx.item_names.lookup_in_slot(item.item) player_name = ctx.player_names[item.player] receive_message = generate_received_text(item_name, player_name) @@ -596,7 +597,7 @@ async def game_watcher(self, ctx): for loc_id in ctx.checked_locations: if loc_id not in ctx.locations_checked: ctx.locations_checked.add(loc_id) - loc_name = ctx.location_names[loc_id] + loc_name = ctx.location_names.lookup_in_slot(loc_id) if loc_name not in location_id_to_level_id: continue diff --git a/worlds/smz3/Client.py b/worlds/smz3/Client.py index 0a248aa5d3f2..3c90ead0064c 100644 --- a/worlds/smz3/Client.py +++ b/worlds/smz3/Client.py @@ -109,7 +109,7 @@ async def game_watcher(self, ctx): location_id = locations_start_id + convertLocSMZ3IDToAPID(item_index) ctx.locations_checked.add(location_id) - location = ctx.location_names[location_id] + location = ctx.location_names.lookup_in_slot(location_id) snes_logger.info(f'New Check: {location} ({len(ctx.locations_checked)}/{len(ctx.missing_locations) + len(ctx.checked_locations)})') await ctx.send_msgs([{"cmd": 'LocationChecks', "locations": [location_id]}]) @@ -132,8 +132,7 @@ async def game_watcher(self, ctx): item_out_ptr += 1 snes_buffered_write(ctx, SMZ3_RECV_PROGRESS_ADDR + recv_progress_addr_table_offset, bytes([item_out_ptr & 0xFF, (item_out_ptr >> 8) & 0xFF])) logging.info('Received %s from %s (%s) (%d/%d in list)' % ( - color(ctx.item_names[item.item], 'red', 'bold'), color(ctx.player_names[item.player], 'yellow'), - ctx.location_names[item.location], item_out_ptr, len(ctx.items_received))) + color(ctx.item_names.lookup_in_slot(item.item), 'red', 'bold'), color(ctx.player_names[item.player], 'yellow'), + ctx.location_names.lookup_in_slot(item.location, item.player), item_out_ptr, len(ctx.items_received))) await snes_flush_writes(ctx) - diff --git a/worlds/smz3/__init__.py b/worlds/smz3/__init__.py index b030e3fa50d2..6056a171d370 100644 --- a/worlds/smz3/__init__.py +++ b/worlds/smz3/__init__.py @@ -68,7 +68,6 @@ class SMZ3World(World): """ game: str = "SMZ3" topology_present = False - data_version = 3 option_definitions = smz3_options item_names: Set[str] = frozenset(TotalSMZ3Item.lookup_name_to_id) location_names: Set[str] diff --git a/worlds/soe/__init__.py b/worlds/soe/__init__.py index 061322650e68..3baed165d821 100644 --- a/worlds/soe/__init__.py +++ b/worlds/soe/__init__.py @@ -176,7 +176,6 @@ class SoEWorld(World): options: SoEOptions settings: typing.ClassVar[SoESettings] topology_present = False - data_version = 5 web = SoEWebWorld() required_client_version = (0, 4, 4) diff --git a/worlds/spire/__init__.py b/worlds/spire/__init__.py index d8a9322ab415..5b0e1e17f23d 100644 --- a/worlds/spire/__init__.py +++ b/worlds/spire/__init__.py @@ -30,7 +30,6 @@ class SpireWorld(World): option_definitions = spire_options game = "Slay the Spire" topology_present = False - data_version = 2 web = SpireWeb() required_client_version = (0, 3, 7) diff --git a/worlds/stardew_valley/__init__.py b/worlds/stardew_valley/__init__.py index dafb1c64730f..61c866631690 100644 --- a/worlds/stardew_valley/__init__.py +++ b/worlds/stardew_valley/__init__.py @@ -73,7 +73,6 @@ class StardewValleyWorld(World): [location.name for location in locations] for group, locations in locations_by_tag.items() } - data_version = 3 required_client_version = (0, 4, 0) options_dataclass = StardewValleyOptions diff --git a/worlds/subnautica/__init__.py b/worlds/subnautica/__init__.py index 08df70d78bbd..856117469e55 100644 --- a/worlds/subnautica/__init__.py +++ b/worlds/subnautica/__init__.py @@ -44,7 +44,6 @@ class SubnauticaWorld(World): location_name_to_id = all_locations options_dataclass = options.SubnauticaOptions options: options.SubnauticaOptions - data_version = 10 required_client_version = (0, 4, 1) creatures_to_scan: List[str] diff --git a/worlds/terraria/__init__.py b/worlds/terraria/__init__.py index ac6b25e51632..abc10a7bb37c 100644 --- a/worlds/terraria/__init__.py +++ b/worlds/terraria/__init__.py @@ -52,11 +52,6 @@ class TerrariaWorld(World): options_dataclass = TerrariaOptions options: TerrariaOptions - # data_version is used to signal that items, locations or their names - # changed. Set this to 0 during development so other games' clients do not - # cache any texts, then increase by 1 for each release that makes changes. - data_version = 2 - item_name_to_id = item_name_to_id location_name_to_id = location_name_to_id diff --git a/worlds/timespinner/__init__.py b/worlds/timespinner/__init__.py index ff7b3515e605..cab6fb648b95 100644 --- a/worlds/timespinner/__init__.py +++ b/worlds/timespinner/__init__.py @@ -39,7 +39,6 @@ class TimespinnerWorld(World): option_definitions = timespinner_options game = "Timespinner" topology_present = True - data_version = 12 web = TimespinnerWebWorld() required_client_version = (0, 4, 2) @@ -228,7 +227,7 @@ def assign_starter_items(self, excluded_items: Set[str]) -> None: non_local_items: Set[str] = self.multiworld.non_local_items[self.player].value local_items: Set[str] = self.multiworld.local_items[self.player].value - local_starter_melee_weapons = tuple(item for item in starter_melee_weapons if + local_starter_melee_weapons = tuple(item for item in starter_melee_weapons if item in local_items or not item in non_local_items) if not local_starter_melee_weapons: if 'Plasma Orb' in non_local_items: diff --git a/worlds/tloz/__init__.py b/worlds/tloz/__init__.py index 7565dc0147ce..a1f9081418e4 100644 --- a/worlds/tloz/__init__.py +++ b/worlds/tloz/__init__.py @@ -68,7 +68,6 @@ class TLoZWorld(World): settings: typing.ClassVar[TLoZSettings] game = "The Legend of Zelda" topology_present = False - data_version = 1 base_id = 7000 web = TLoZWeb() diff --git a/worlds/undertale/__init__.py b/worlds/undertale/__init__.py index 0694456a6b12..b87d3ac01e8f 100644 --- a/worlds/undertale/__init__.py +++ b/worlds/undertale/__init__.py @@ -52,8 +52,6 @@ class UndertaleWorld(World): item_name_to_id = {name: data.code for name, data in item_table.items()} location_name_to_id = {name: data.id for name, data in advancement_table.items()} - data_version = 7 - def _get_undertale_data(self): return { "world_seed": self.multiworld.per_slot_randoms[self.player].getrandbits(32), diff --git a/worlds/v6/__init__.py b/worlds/v6/__init__.py index 30a76f82cce6..3d3ee8cf58fd 100644 --- a/worlds/v6/__init__.py +++ b/worlds/v6/__init__.py @@ -34,8 +34,6 @@ class V6World(World): item_name_to_id = item_table location_name_to_id = location_table - data_version = 1 - area_connections: typing.Dict[int, int] area_cost_map: typing.Dict[int,int] diff --git a/worlds/yoshisisland/Client.py b/worlds/yoshisisland/Client.py index 1aff36c553c7..2a710b046a70 100644 --- a/worlds/yoshisisland/Client.py +++ b/worlds/yoshisisland/Client.py @@ -116,7 +116,7 @@ async def game_watcher(self, ctx: "SNIContext") -> None: for new_check_id in new_checks: ctx.locations_checked.add(new_check_id) - location = ctx.location_names[new_check_id] + location = ctx.location_names.lookup_in_slot(new_check_id) total_locations = len(ctx.missing_locations) + len(ctx.checked_locations) snes_logger.info(f"New Check: {location} ({len(ctx.locations_checked)}/{total_locations})") await ctx.send_msgs([{"cmd": "LocationChecks", "locations": [new_check_id]}]) @@ -127,9 +127,9 @@ async def game_watcher(self, ctx: "SNIContext") -> None: item = ctx.items_received[recv_index] recv_index += 1 logging.info("Received %s from %s (%s) (%d/%d in list)" % ( - color(ctx.item_names[item.item], "red", "bold"), + color(ctx.item_names.lookup_in_slot(item.item), "red", "bold"), color(ctx.player_names[item.player], "yellow"), - ctx.location_names[item.location], recv_index, len(ctx.items_received))) + ctx.location_names.lookup_in_slot(item.location, item.player), recv_index, len(ctx.items_received))) snes_buffered_write(ctx, ITEMQUEUE_HIGH, pack("H", recv_index)) if item.item in item_values: diff --git a/worlds/zillion/__init__.py b/worlds/zillion/__init__.py index cce120d7e3f4..205cc9ad6ba1 100644 --- a/worlds/zillion/__init__.py +++ b/worlds/zillion/__init__.py @@ -86,11 +86,6 @@ class ZillionWorld(World): item_name_to_id = _item_name_to_id location_name_to_id = _loc_name_to_id - # increment this every time something in your world's names/id mappings changes. - # While this is set to 0 in *any* AutoWorld, the entire DataPackage is considered in testing mode and will be - # retrieved by clients on every connection. - data_version = 1 - logger: logging.Logger class LogStreamInterface: From 2a5de8567e020c0d579bfffd68543c2b0cdc6ec0 Mon Sep 17 00:00:00 2001 From: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> Date: Sat, 1 Jun 2024 07:07:43 -0400 Subject: [PATCH 41/64] Docs: Making option description more readable and accurate (#3426) --- Options.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/Options.py b/Options.py index 7f480cbaae8d..995473243959 100644 --- a/Options.py +++ b/Options.py @@ -910,8 +910,10 @@ class Accessibility(Choice): class ProgressionBalancing(NamedRange): - """A system that can move progression earlier, to try and prevent the player from getting stuck and bored early. - A lower setting means more getting stuck. A higher setting means less getting stuck.""" + """ + A system that can move progression earlier, to try and prevent the player from getting stuck and bored early. + A lower setting means more getting stuck. A higher setting means less getting stuck. + """ default = 50 range_start = 0 range_end = 99 @@ -984,7 +986,7 @@ class LocalItems(ItemSet): class NonLocalItems(ItemSet): """Forces these items to be outside their native world.""" - display_name = "Not Local Items" + display_name = "Non-local Items" class StartInventory(ItemDict): From f2587d5d2773870a0323a92f895867959a997109 Mon Sep 17 00:00:00 2001 From: Louis M Date: Sat, 1 Jun 2024 07:09:34 -0400 Subject: [PATCH 42/64] Aquatia: Locations name changed due to typo's, grammar, or inconsistencies (#3421) --- worlds/aquaria/Items.py | 68 +-- worlds/aquaria/Locations.py | 364 +++++++-------- worlds/aquaria/Options.py | 2 +- worlds/aquaria/Regions.py | 438 +++++++++--------- worlds/aquaria/__init__.py | 22 +- worlds/aquaria/docs/en_Aquaria.md | 8 +- worlds/aquaria/test/__init__.py | 294 ++++++------ worlds/aquaria/test/test_beast_form_access.py | 38 +- worlds/aquaria/test/test_bind_song_access.py | 26 +- .../test/test_bind_song_option_access.py | 36 +- .../aquaria/test/test_confined_home_water.py | 4 +- worlds/aquaria/test/test_dual_song_access.py | 8 +- .../aquaria/test/test_energy_form_access.py | 68 +-- worlds/aquaria/test/test_fish_form_access.py | 20 +- worlds/aquaria/test/test_li_song_access.py | 42 +- worlds/aquaria/test/test_light_access.py | 56 +-- .../aquaria/test/test_nature_form_access.py | 56 +-- ...st_no_progression_hard_hidden_locations.py | 40 +- .../test_progression_hard_hidden_locations.py | 40 +- .../aquaria/test/test_spirit_form_access.py | 30 +- worlds/aquaria/test/test_sun_form_access.py | 6 +- .../test_unconfine_home_water_via_both.py | 4 +- ...st_unconfine_home_water_via_energy_door.py | 4 +- ...st_unconfine_home_water_via_transturtle.py | 2 +- 24 files changed, 838 insertions(+), 838 deletions(-) diff --git a/worlds/aquaria/Items.py b/worlds/aquaria/Items.py index 5494c87e8cf4..34557d95d00d 100644 --- a/worlds/aquaria/Items.py +++ b/worlds/aquaria/Items.py @@ -77,41 +77,41 @@ def __init__(self, id: int, count: int, type: ItemType, group: ItemGroup): item_table = { # name: ID, Nb, Item Type, Item Group "Anemone": ItemData(698000, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_anemone - "Arnassi statue": ItemData(698001, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_arnassi_statue - "Big seed": ItemData(698002, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_big_seed - "Glowing seed": ItemData(698003, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_bio_seed - "Black pearl": ItemData(698004, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_blackpearl - "Baby blaster": ItemData(698005, 1, ItemType.NORMAL, ItemGroup.UTILITY), # collectible_blaster - "Crab armor": ItemData(698006, 1, ItemType.NORMAL, ItemGroup.UTILITY), # collectible_crab_costume - "Baby dumbo": ItemData(698007, 1, ItemType.PROGRESSION, ItemGroup.UTILITY), # collectible_dumbo + "Arnassi Statue": ItemData(698001, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_arnassi_statue + "Big Seed": ItemData(698002, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_big_seed + "Glowing Seed": ItemData(698003, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_bio_seed + "Black Pearl": ItemData(698004, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_blackpearl + "Baby Blaster": ItemData(698005, 1, ItemType.NORMAL, ItemGroup.UTILITY), # collectible_blaster + "Crab Armor": ItemData(698006, 1, ItemType.NORMAL, ItemGroup.UTILITY), # collectible_crab_costume + "Baby Dumbo": ItemData(698007, 1, ItemType.PROGRESSION, ItemGroup.UTILITY), # collectible_dumbo "Tooth": ItemData(698008, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_energy_boss - "Energy statue": ItemData(698009, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_energy_statue - "Krotite armor": ItemData(698010, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_energy_temple - "Golden starfish": ItemData(698011, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_gold_star - "Golden gear": ItemData(698012, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_golden_gear - "Jelly beacon": ItemData(698013, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_jelly_beacon - "Jelly costume": ItemData(698014, 1, ItemType.NORMAL, ItemGroup.UTILITY), # collectible_jelly_costume - "Jelly plant": ItemData(698015, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_jelly_plant - "Mithalas doll": ItemData(698016, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_mithala_doll - "Mithalan dress": ItemData(698017, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_mithalan_costume - "Mithalas banner": ItemData(698018, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_mithalas_banner - "Mithalas pot": ItemData(698019, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_mithalas_pot - "Mutant costume": ItemData(698020, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_mutant_costume - "Baby nautilus": ItemData(698021, 1, ItemType.NORMAL, ItemGroup.UTILITY), # collectible_nautilus - "Baby piranha": ItemData(698022, 1, ItemType.NORMAL, ItemGroup.UTILITY), # collectible_piranha + "Energy Statue": ItemData(698009, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_energy_statue + "Krotite Armor": ItemData(698010, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_energy_temple + "Golden Starfish": ItemData(698011, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_gold_star + "Golden Gear": ItemData(698012, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_golden_gear + "Jelly Beacon": ItemData(698013, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_jelly_beacon + "Jelly Costume": ItemData(698014, 1, ItemType.NORMAL, ItemGroup.UTILITY), # collectible_jelly_costume + "Jelly Plant": ItemData(698015, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_jelly_plant + "Mithalas Doll": ItemData(698016, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_mithala_doll + "Mithalan Dress": ItemData(698017, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_mithalan_costume + "Mithalas Banner": ItemData(698018, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_mithalas_banner + "Mithalas Pot": ItemData(698019, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_mithalas_pot + "Mutant Costume": ItemData(698020, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_mutant_costume + "Baby Nautilus": ItemData(698021, 1, ItemType.NORMAL, ItemGroup.UTILITY), # collectible_nautilus + "Baby Piranha": ItemData(698022, 1, ItemType.NORMAL, ItemGroup.UTILITY), # collectible_piranha "Arnassi Armor": ItemData(698023, 1, ItemType.NORMAL, ItemGroup.UTILITY), # collectible_seahorse_costume - "Seed bag": ItemData(698024, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_seed_bag + "Seed Bag": ItemData(698024, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_seed_bag "King's Skull": ItemData(698025, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_skull - "Song plant spore": ItemData(698026, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_spore_seed - "Stone head": ItemData(698027, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_stone_head - "Sun key": ItemData(698028, 1, ItemType.NORMAL, ItemGroup.COLLECTIBLE), # collectible_sun_key - "Girl costume": ItemData(698029, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_teen_costume - "Odd container": ItemData(698030, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_treasure_chest + "Song Plant Spore": ItemData(698026, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_spore_seed + "Stone Head": ItemData(698027, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_stone_head + "Sun Key": ItemData(698028, 1, ItemType.NORMAL, ItemGroup.COLLECTIBLE), # collectible_sun_key + "Girl Costume": ItemData(698029, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_teen_costume + "Odd Container": ItemData(698030, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_treasure_chest "Trident": ItemData(698031, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_trident_head - "Turtle egg": ItemData(698032, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_turtle_egg - "Jelly egg": ItemData(698033, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_upsidedown_seed - "Urchin costume": ItemData(698034, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_urchin_costume - "Baby walker": ItemData(698035, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_walker + "Turtle Egg": ItemData(698032, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_turtle_egg + "Jelly Egg": ItemData(698033, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_upsidedown_seed + "Urchin Costume": ItemData(698034, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_urchin_costume + "Baby Walker": ItemData(698035, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_walker "Vedha's Cure-All-All": ItemData(698036, 1, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_Vedha'sCure-All "Zuuna's perogi": ItemData(698037, 1, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_Zuuna'sperogi "Arcane poultice": ItemData(698038, 7, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_arcanepoultice @@ -206,9 +206,9 @@ def __init__(self, id: int, count: int, type: ItemType, group: ItemGroup): "Transturtle Open Water top right": ItemData(698127, 1, ItemType.PROGRESSION, ItemGroup.TURTLE), # transport_openwater03 "Transturtle Forest bottom left": ItemData(698128, 1, ItemType.PROGRESSION, ItemGroup.TURTLE), # transport_forest04 - "Transturtle Home water": ItemData(698129, 1, ItemType.NORMAL, ItemGroup.TURTLE), # transport_mainarea + "Transturtle Home Water": ItemData(698129, 1, ItemType.NORMAL, ItemGroup.TURTLE), # transport_mainarea "Transturtle Abyss right": ItemData(698130, 1, ItemType.PROGRESSION, ItemGroup.TURTLE), # transport_abyss03 "Transturtle Final Boss": ItemData(698131, 1, ItemType.PROGRESSION, ItemGroup.TURTLE), # transport_finalboss - "Transturtle Simon says": ItemData(698132, 1, ItemType.PROGRESSION, ItemGroup.TURTLE), # transport_forest05 - "Transturtle Arnassi ruins": ItemData(698133, 1, ItemType.PROGRESSION, ItemGroup.TURTLE), # transport_seahorse + "Transturtle Simon Says": ItemData(698132, 1, ItemType.PROGRESSION, ItemGroup.TURTLE), # transport_forest05 + "Transturtle Arnassi Ruins": ItemData(698133, 1, ItemType.PROGRESSION, ItemGroup.TURTLE), # transport_seahorse } diff --git a/worlds/aquaria/Locations.py b/worlds/aquaria/Locations.py index e4f6f104ccf9..7360efde065e 100644 --- a/worlds/aquaria/Locations.py +++ b/worlds/aquaria/Locations.py @@ -29,213 +29,213 @@ def __init__(self, player: int, name="", code=None, parent=None) -> None: class AquariaLocations: locations_verse_cave_r = { - "Verse cave, bulb in the skeleton room": 698107, - "Verse cave, bulb in the path left of the skeleton room": 698108, - "Verse cave right area, Big Seed": 698175, + "Verse Cave, bulb in the skeleton room": 698107, + "Verse Cave, bulb in the path left of the skeleton room": 698108, + "Verse Cave right area, Big Seed": 698175, } locations_verse_cave_l = { - "Verse cave, the Naija hint about here shield ability": 698200, - "Verse cave left area, bulb in the center part": 698021, - "Verse cave left area, bulb in the right part": 698022, - "Verse cave left area, bulb under the rock at the end of the path": 698023, + "Verse Cave, the Naija hint about the shield ability": 698200, + "Verse Cave left area, bulb in the center part": 698021, + "Verse Cave left area, bulb in the right part": 698022, + "Verse Cave left area, bulb under the rock at the end of the path": 698023, } locations_home_water = { - "Home water, bulb below the grouper fish": 698058, - "Home water, bulb in the path below Nautilus Prime": 698059, - "Home water, bulb in the little room above the grouper fish": 698060, - "Home water, bulb in the end of the left path from the verse cave": 698061, - "Home water, bulb in the top left path": 698062, - "Home water, bulb in the bottom left room": 698063, - "Home water, bulb close to the Naija's home": 698064, - "Home water, bulb under the rock in the left path from the verse cave": 698065, + "Home Water, bulb below the grouper fish": 698058, + "Home Water, bulb in the path below Nautilus Prime": 698059, + "Home Water, bulb in the little room above the grouper fish": 698060, + "Home Water, bulb in the end of the left path from the Verse Cave": 698061, + "Home Water, bulb in the top left path": 698062, + "Home Water, bulb in the bottom left room": 698063, + "Home Water, bulb close to Naija's Home": 698064, + "Home Water, bulb under the rock in the left path from the Verse Cave": 698065, } locations_home_water_nautilus = { - "Home water, Nautilus Egg": 698194, + "Home Water, Nautilus Egg": 698194, } locations_home_water_transturtle = { - "Home water, Transturtle": 698213, + "Home Water, Transturtle": 698213, } locations_naija_home = { - "Naija's home, bulb after the energy door": 698119, - "Naija's home, bulb under the rock at the right of the main path": 698120, + "Naija's Home, bulb after the energy door": 698119, + "Naija's Home, bulb under the rock at the right of the main path": 698120, } locations_song_cave = { - "Song cave, Erulian spirit": 698206, - "Song cave, bulb in the top left part": 698071, - "Song cave, bulb in the big anemone room": 698072, - "Song cave, bulb in the path to the singing statues": 698073, - "Song cave, bulb under the rock in the path to the singing statues": 698074, - "Song cave, bulb under the rock close to the song door": 698075, - "Song cave, Verse egg": 698160, - "Song cave, Jelly beacon": 698178, - "Song cave, Anemone seed": 698162, + "Song Cave, Erulian spirit": 698206, + "Song Cave, bulb in the top left part": 698071, + "Song Cave, bulb in the big anemone room": 698072, + "Song Cave, bulb in the path to the singing statues": 698073, + "Song Cave, bulb under the rock in the path to the singing statues": 698074, + "Song Cave, bulb under the rock close to the song door": 698075, + "Song Cave, Verse Egg": 698160, + "Song Cave, Jelly Beacon": 698178, + "Song Cave, Anemone Seed": 698162, } locations_energy_temple_1 = { - "Energy temple first area, beating the energy statue": 698205, - "Energy temple first area, bulb in the bottom room blocked by a rock": 698027, + "Energy Temple first area, beating the Energy Statue": 698205, + "Energy Temple first area, bulb in the bottom room blocked by a rock": 698027, } locations_energy_temple_idol = { - "Energy temple first area, Energy Idol": 698170, + "Energy Temple first area, Energy Idol": 698170, } locations_energy_temple_2 = { - "Energy temple second area, bulb under the rock": 698028, + "Energy Temple second area, bulb under the rock": 698028, } locations_energy_temple_altar = { - "Energy temple bottom entrance, Krotite armor": 698163, + "Energy Temple bottom entrance, Krotite Armor": 698163, } locations_energy_temple_3 = { - "Energy temple third area, bulb in the bottom path": 698029, + "Energy Temple third area, bulb in the bottom path": 698029, } locations_energy_temple_boss = { - "Energy temple boss area, Fallen god tooth": 698169, + "Energy Temple boss area, Fallen God Tooth": 698169, } locations_energy_temple_blaster_room = { - "Energy temple blaster room, Blaster egg": 698195, + "Energy Temple blaster room, Blaster Egg": 698195, } locations_openwater_tl = { - "Open water top left area, bulb under the rock in the right path": 698001, - "Open water top left area, bulb under the rock in the left path": 698002, - "Open water top left area, bulb to the right of the save cristal": 698003, + "Open Water top left area, bulb under the rock in the right path": 698001, + "Open Water top left area, bulb under the rock in the left path": 698002, + "Open Water top left area, bulb to the right of the save crystal": 698003, } locations_openwater_tr = { - "Open water top right area, bulb in the small path before Mithalas": 698004, - "Open water top right area, bulb in the path from the left entrance": 698005, - "Open water top right area, bulb in the clearing close to the bottom exit": 698006, - "Open water top right area, bulb in the big clearing close to the save cristal": 698007, - "Open water top right area, bulb in the big clearing to the top exit": 698008, - "Open water top right area, first urn in the Mithalas exit": 698148, - "Open water top right area, second urn in the Mithalas exit": 698149, - "Open water top right area, third urn in the Mithalas exit": 698150, + "Open Water top right area, bulb in the small path before Mithalas": 698004, + "Open Water top right area, bulb in the path from the left entrance": 698005, + "Open Water top right area, bulb in the clearing close to the bottom exit": 698006, + "Open Water top right area, bulb in the big clearing close to the save crystal": 698007, + "Open Water top right area, bulb in the big clearing to the top exit": 698008, + "Open Water top right area, first urn in the Mithalas exit": 698148, + "Open Water top right area, second urn in the Mithalas exit": 698149, + "Open Water top right area, third urn in the Mithalas exit": 698150, } locations_openwater_tr_turtle = { - "Open water top right area, bulb in the turtle room": 698009, - "Open water top right area, Transturtle": 698211, + "Open Water top right area, bulb in the turtle room": 698009, + "Open Water top right area, Transturtle": 698211, } locations_openwater_bl = { - "Open water bottom left area, bulb behind the chomper fish": 698011, - "Open water bottom left area, bulb inside the lowest fish pass": 698010, + "Open Water bottom left area, bulb behind the chomper fish": 698011, + "Open Water bottom left area, bulb inside the lowest fish pass": 698010, } locations_skeleton_path = { - "Open water skeleton path, bulb close to the right exit": 698012, - "Open water skeleton path, bulb behind the chomper fish": 698013, + "Open Water skeleton path, bulb close to the right exit": 698012, + "Open Water skeleton path, bulb behind the chomper fish": 698013, } locations_skeleton_path_sc = { - "Open water skeleton path, King skull": 698177, + "Open Water skeleton path, King Skull": 698177, } locations_arnassi = { "Arnassi Ruins, bulb in the right part": 698014, "Arnassi Ruins, bulb in the left part": 698015, "Arnassi Ruins, bulb in the center part": 698016, - "Arnassi ruins, Song plant spore on the top of the ruins": 698179, - "Arnassi ruins, Arnassi Armor": 698191, + "Arnassi Ruins, Song Plant Spore": 698179, + "Arnassi Ruins, Arnassi Armor": 698191, } locations_arnassi_path = { - "Arnassi Ruins, Arnassi statue": 698164, + "Arnassi Ruins, Arnassi Statue": 698164, "Arnassi Ruins, Transturtle": 698217, } locations_arnassi_crab_boss = { - "Arnassi ruins, Crab armor": 698187, + "Arnassi Ruins, Crab Armor": 698187, } locations_simon = { - "Kelp forest, beating Simon says": 698156, - "Simon says area, Transturtle": 698216, + "Simon Says area, beating Simon Says": 698156, + "Simon Says area, Transturtle": 698216, } locations_mithalas_city = { - "Mithalas city, first bulb in the left city part": 698030, - "Mithalas city, second bulb in the left city part": 698035, - "Mithalas city, bulb in the right part": 698031, - "Mithalas city, bulb at the top of the city": 698033, - "Mithalas city, first bulb in a broken home": 698034, - "Mithalas city, second bulb in a broken home": 698041, - "Mithalas city, bulb in the bottom left part": 698037, - "Mithalas city, first bulb in one of the homes": 698038, - "Mithalas city, second bulb in one of the homes": 698039, - "Mithalas city, first urn in one of the homes": 698123, - "Mithalas city, second urn in one of the homes": 698124, - "Mithalas city, first urn in the city reserve": 698125, - "Mithalas city, second urn in the city reserve": 698126, - "Mithalas city, third urn in the city reserve": 698127, + "Mithalas City, first bulb in the left city part": 698030, + "Mithalas City, second bulb in the left city part": 698035, + "Mithalas City, bulb in the right part": 698031, + "Mithalas City, bulb at the top of the city": 698033, + "Mithalas City, first bulb in a broken home": 698034, + "Mithalas City, second bulb in a broken home": 698041, + "Mithalas City, bulb in the bottom left part": 698037, + "Mithalas City, first bulb in one of the homes": 698038, + "Mithalas City, second bulb in one of the homes": 698039, + "Mithalas City, first urn in one of the homes": 698123, + "Mithalas City, second urn in one of the homes": 698124, + "Mithalas City, first urn in the city reserve": 698125, + "Mithalas City, second urn in the city reserve": 698126, + "Mithalas City, third urn in the city reserve": 698127, } locations_mithalas_city_top_path = { - "Mithalas city, first bulb at the end of the top path": 698032, - "Mithalas city, second bulb at the end of the top path": 698040, - "Mithalas city, bulb in the top path": 698036, - "Mithalas city, Mithalas pot": 698174, - "Mithalas city, urn in the cathedral flower tube entrance": 698128, + "Mithalas City, first bulb at the end of the top path": 698032, + "Mithalas City, second bulb at the end of the top path": 698040, + "Mithalas City, bulb in the top path": 698036, + "Mithalas City, Mithalas Pot": 698174, + "Mithalas City, urn in the Cathedral flower tube entrance": 698128, } locations_mithalas_city_fishpass = { - "Mithalas city, Doll": 698173, - "Mithalas city, urn inside a home fish pass": 698129, + "Mithalas City, Doll": 698173, + "Mithalas City, urn inside a home fish pass": 698129, } locations_cathedral_l = { - "Mithalas city castle, bulb in the flesh hole": 698042, - "Mithalas city castle, Blue banner": 698165, - "Mithalas city castle, urn in the bedroom": 698130, - "Mithalas city castle, first urn of the single lamp path": 698131, - "Mithalas city castle, second urn of the single lamp path": 698132, - "Mithalas city castle, urn in the bottom room": 698133, - "Mithalas city castle, first urn on the entrance path": 698134, - "Mithalas city castle, second urn on the entrance path": 698135, + "Mithalas City Castle, bulb in the flesh hole": 698042, + "Mithalas City Castle, Blue banner": 698165, + "Mithalas City Castle, urn in the bedroom": 698130, + "Mithalas City Castle, first urn of the single lamp path": 698131, + "Mithalas City Castle, second urn of the single lamp path": 698132, + "Mithalas City Castle, urn in the bottom room": 698133, + "Mithalas City Castle, first urn on the entrance path": 698134, + "Mithalas City Castle, second urn on the entrance path": 698135, } locations_cathedral_l_tube = { - "Mithalas castle, beating the priests": 698208, + "Mithalas City Castle, beating the Priests": 698208, } locations_cathedral_l_sc = { - "Mithalas city castle, Trident head": 698183, + "Mithalas City Castle, Trident Head": 698183, } locations_cathedral_r = { - "Mithalas cathedral, first urn in the top right room": 698136, - "Mithalas cathedral, second urn in the top right room": 698137, - "Mithalas cathedral, third urn in the top right room": 698138, - "Mithalas cathedral, urn in the flesh room with fleas": 698139, - "Mithalas cathedral, first urn in the bottom right path": 698140, - "Mithalas cathedral, second urn in the bottom right path": 698141, - "Mithalas cathedral, urn behind the flesh vein": 698142, - "Mithalas cathedral, urn in the top left eyes boss room": 698143, - "Mithalas cathedral, first urn in the path behind the flesh vein": 698144, - "Mithalas cathedral, second urn in the path behind the flesh vein": 698145, - "Mithalas cathedral, third urn in the path behind the flesh vein": 698146, - "Mithalas cathedral, one of the urns in the top right room": 698147, - "Mithalas cathedral, Mithalan Dress": 698189, - "Mithalas cathedral right area, urn below the left entrance": 698198, + "Mithalas Cathedral, first urn in the top right room": 698136, + "Mithalas Cathedral, second urn in the top right room": 698137, + "Mithalas Cathedral, third urn in the top right room": 698138, + "Mithalas Cathedral, urn in the flesh room with fleas": 698139, + "Mithalas Cathedral, first urn in the bottom right path": 698140, + "Mithalas Cathedral, second urn in the bottom right path": 698141, + "Mithalas Cathedral, urn behind the flesh vein": 698142, + "Mithalas Cathedral, urn in the top left eyes boss room": 698143, + "Mithalas Cathedral, first urn in the path behind the flesh vein": 698144, + "Mithalas Cathedral, second urn in the path behind the flesh vein": 698145, + "Mithalas Cathedral, third urn in the path behind the flesh vein": 698146, + "Mithalas Cathedral, fourth urn in the top right room": 698147, + "Mithalas Cathedral, Mithalan Dress": 698189, + "Mithalas Cathedral right area, urn below the left entrance": 698198, } locations_cathedral_underground = { - "Cathedral underground, bulb in the center part": 698113, - "Cathedral underground, first bulb in the top left part": 698114, - "Cathedral underground, second bulb in the top left part": 698115, - "Cathedral underground, third bulb in the top left part": 698116, - "Cathedral underground, bulb close to the save cristal": 698117, - "Cathedral underground, bulb in the bottom right path": 698118, + "Cathedral Underground, bulb in the center part": 698113, + "Cathedral Underground, first bulb in the top left part": 698114, + "Cathedral Underground, second bulb in the top left part": 698115, + "Cathedral Underground, third bulb in the top left part": 698116, + "Cathedral Underground, bulb close to the save crystal": 698117, + "Cathedral Underground, bulb in the bottom right path": 698118, } locations_cathedral_boss = { @@ -250,8 +250,8 @@ class AquariaLocations: } locations_forest_tl_fp = { - "Kelp Forest top left area, bulb close to the Verse egg": 698047, - "Kelp forest top left area, Verse egg": 698158, + "Kelp Forest top left area, bulb close to the Verse Egg": 698047, + "Kelp Forest top left area, Verse Egg": 698158, } locations_forest_tr = { @@ -260,7 +260,7 @@ class AquariaLocations: "Kelp Forest top right area, bulb in the left path's big room": 698051, "Kelp Forest top right area, bulb in the left path's small room": 698052, "Kelp Forest top right area, bulb at the top of the center clearing": 698053, - "Kelp forest top right area, Black pearl": 698167, + "Kelp Forest top right area, Black Pearl": 698167, } locations_forest_tr_fp = { @@ -269,16 +269,16 @@ class AquariaLocations: locations_forest_bl = { "Kelp Forest bottom left area, bulb close to the spirit crystals": 698054, - "Kelp forest bottom left area, Walker baby": 698186, + "Kelp Forest bottom left area, Walker baby": 698186, "Kelp Forest bottom left area, Transturtle": 698212, } locations_forest_br = { - "Kelp forest bottom right area, Odd Container": 698168, + "Kelp Forest bottom right area, Odd Container": 698168, } locations_forest_boss = { - "Kelp forest boss area, beating Drunian God": 698204, + "Kelp Forest boss area, beating Drunian God": 698204, } locations_forest_boss_entrance = { @@ -286,7 +286,7 @@ class AquariaLocations: } locations_forest_fish_cave = { - "Kelp Forest bottom left area, Fish cave puzzle": 698207, + "Kelp Forest bottom left area, Fish Cave puzzle": 698207, } locations_forest_sprite_cave = { @@ -295,7 +295,7 @@ class AquariaLocations: locations_forest_sprite_cave_tube = { "Kelp Forest sprite cave, bulb in the second room": 698057, - "Kelp Forest Sprite Cave, Seed bag": 698176, + "Kelp Forest sprite cave, Seed Bag": 698176, } locations_mermog_cave = { @@ -307,14 +307,14 @@ class AquariaLocations: } locations_veil_tl = { - "The veil top left area, In the Li cave": 698199, - "The veil top left area, bulb under the rock in the top right path": 698078, - "The veil top left area, bulb hidden behind the blocking rock": 698076, - "The veil top left area, Transturtle": 698209, + "The Veil top left area, In Li's cave": 698199, + "The Veil top left area, bulb under the rock in the top right path": 698078, + "The Veil top left area, bulb hidden behind the blocking rock": 698076, + "The Veil top left area, Transturtle": 698209, } locations_veil_tl_fp = { - "The veil top left area, bulb inside the fish pass": 698077, + "The Veil top left area, bulb inside the fish pass": 698077, } locations_turtle_cave = { @@ -322,56 +322,56 @@ class AquariaLocations: } locations_turtle_cave_bubble = { - "Turtle cave, bulb in bubble cliff": 698000, - "Turtle cave, Urchin costume": 698193, + "Turtle cave, bulb in Bubble Cliff": 698000, + "Turtle cave, Urchin Costume": 698193, } locations_veil_tr_r = { - "The veil top right area, bulb in the middle of the wall jump cliff": 698079, - "The veil top right area, golden starfish at the bottom right of the bottom path": 698180, + "The Veil top right area, bulb in the middle of the wall jump cliff": 698079, + "The Veil top right area, Golden Starfish": 698180, } locations_veil_tr_l = { - "The veil top right area, bulb in the top of the water fall": 698080, - "The veil top right area, Transturtle": 698210, + "The Veil top right area, bulb in the top of the waterfall": 698080, + "The Veil top right area, Transturtle": 698210, } locations_veil_bl = { - "The veil bottom area, bulb in the left path": 698082, + "The Veil bottom area, bulb in the left path": 698082, } locations_veil_b_sc = { - "The veil bottom area, bulb in the spirit path": 698081, + "The Veil bottom area, bulb in the spirit path": 698081, } locations_veil_bl_fp = { - "The veil bottom area, Verse egg": 698157, + "The Veil bottom area, Verse Egg": 698157, } locations_veil_br = { - "The veil bottom area, Stone Head": 698181, + "The Veil bottom area, Stone Head": 698181, } locations_octo_cave_t = { - "Octopus cave, Dumbo Egg": 698196, + "Octopus Cave, Dumbo Egg": 698196, } locations_octo_cave_b = { - "Octopus cave, bulb in the path below the octopus cave path": 698122, + "Octopus Cave, bulb in the path below the Octopus Cave path": 698122, } locations_sun_temple_l = { - "Sun temple, bulb in the top left part": 698094, - "Sun temple, bulb in the top right part": 698095, - "Sun temple, bulb at the top of the high dark room": 698096, - "Sun temple, Golden Gear": 698171, + "Sun Temple, bulb in the top left part": 698094, + "Sun Temple, bulb in the top right part": 698095, + "Sun Temple, bulb at the top of the high dark room": 698096, + "Sun Temple, Golden Gear": 698171, } locations_sun_temple_r = { - "Sun temple, first bulb of the temple": 698091, - "Sun temple, bulb on the left part": 698092, - "Sun temple, bulb in the hidden room of the right part": 698093, - "Sun temple, Sun key": 698182, + "Sun Temple, first bulb of the temple": 698091, + "Sun Temple, bulb on the left part": 698092, + "Sun Temple, bulb in the hidden room of the right part": 698093, + "Sun Temple, Sun Key": 698182, } locations_sun_temple_boss_path = { @@ -382,13 +382,13 @@ class AquariaLocations: } locations_sun_temple_boss = { - "Sun temple boss area, beating Sun God": 698203, + "Sun Temple boss area, beating Sun God": 698203, } locations_abyss_l = { "Abyss left area, bulb in hidden path room": 698024, "Abyss left area, bulb in the right part": 698025, - "Abyss left area, Glowing seed": 698166, + "Abyss left area, Glowing Seed": 698166, "Abyss left area, Glowing Plant": 698172, } @@ -405,87 +405,87 @@ class AquariaLocations: } locations_ice_cave = { - "Ice cave, bulb in the room to the right": 698083, - "Ice cave, First bulbs in the top exit room": 698084, - "Ice cave, Second bulbs in the top exit room": 698085, - "Ice cave, third bulbs in the top exit room": 698086, - "Ice cave, bulb in the left room": 698087, + "Ice Cave, bulb in the room to the right": 698083, + "Ice Cave, first bulb in the top exit room": 698084, + "Ice Cave, second bulb in the top exit room": 698085, + "Ice Cave, third bulb in the top exit room": 698086, + "Ice Cave, bulb in the left room": 698087, } locations_bubble_cave = { - "Bubble cave, bulb in the left cave wall": 698089, - "Bubble cave, bulb in the right cave wall (behind the ice cristal)": 698090, + "Bubble Cave, bulb in the left cave wall": 698089, + "Bubble Cave, bulb in the right cave wall (behind the ice crystal)": 698090, } locations_bubble_cave_boss = { - "Bubble cave, Verse egg": 698161, + "Bubble Cave, Verse Egg": 698161, } locations_king_jellyfish_cave = { - "King Jellyfish cave, bulb in the right path from King Jelly": 698088, - "King Jellyfish cave, Jellyfish Costume": 698188, + "King Jellyfish Cave, bulb in the right path from King Jelly": 698088, + "King Jellyfish Cave, Jellyfish Costume": 698188, } locations_whale = { - "The whale, Verse egg": 698159, + "The Whale, Verse Egg": 698159, } locations_sunken_city_r = { - "Sunken city right area, crate close to the save cristal": 698154, - "Sunken city right area, crate in the left bottom room": 698155, + "Sunken City right area, crate close to the save crystal": 698154, + "Sunken City right area, crate in the left bottom room": 698155, } locations_sunken_city_l = { - "Sunken city left area, crate in the little pipe room": 698151, - "Sunken city left area, crate close to the save cristal": 698152, - "Sunken city left area, crate before the bedroom": 698153, + "Sunken City left area, crate in the little pipe room": 698151, + "Sunken City left area, crate close to the save crystal": 698152, + "Sunken City left area, crate before the bedroom": 698153, } locations_sunken_city_l_bedroom = { - "Sunken city left area, Girl Costume": 698192, + "Sunken City left area, Girl Costume": 698192, } locations_sunken_city_boss = { - "Sunken city, bulb on the top of the boss area (boiler room)": 698043, + "Sunken City, bulb on top of the boss area": 698043, } locations_body_c = { - "The body center area, breaking li cage": 698201, - "The body main area, bulb on the main path blocking tube": 698097, + "The Body center area, breaking Li's cage": 698201, + "The Body main area, bulb on the main path blocking tube": 698097, } locations_body_l = { - "The body left area, first bulb in the top face room": 698066, - "The body left area, second bulb in the top face room": 698069, - "The body left area, bulb below the water stream": 698067, - "The body left area, bulb in the top path to the top face room": 698068, - "The body left area, bulb in the bottom face room": 698070, + "The Body left area, first bulb in the top face room": 698066, + "The Body left area, second bulb in the top face room": 698069, + "The Body left area, bulb below the water stream": 698067, + "The Body left area, bulb in the top path to the top face room": 698068, + "The Body left area, bulb in the bottom face room": 698070, } locations_body_rt = { - "The body right area, bulb in the top face room": 698100, + "The Body right area, bulb in the top face room": 698100, } locations_body_rb = { - "The body right area, bulb in the top path to the bottom face room": 698098, - "The body right area, bulb in the bottom face room": 698099, + "The Body right area, bulb in the top path to the bottom face room": 698098, + "The Body right area, bulb in the bottom face room": 698099, } locations_body_b = { - "The body bottom area, bulb in the Jelly Zap room": 698101, - "The body bottom area, bulb in the nautilus room": 698102, - "The body bottom area, Mutant Costume": 698190, + "The Body bottom area, bulb in the Jelly Zap room": 698101, + "The Body bottom area, bulb in the nautilus room": 698102, + "The Body bottom area, Mutant Costume": 698190, } locations_final_boss_tube = { - "Final boss area, first bulb in the turtle room": 698103, - "Final boss area, second bulbs in the turtle room": 698104, - "Final boss area, third bulbs in the turtle room": 698105, - "Final boss area, Transturtle": 698215, + "Final Boss area, first bulb in the turtle room": 698103, + "Final Boss area, second bulb in the turtle room": 698104, + "Final Boss area, third bulb in the turtle room": 698105, + "Final Boss area, Transturtle": 698215, } locations_final_boss = { - "Final boss area, bulb in the boss third form room": 698106, + "Final Boss area, bulb in the boss third form room": 698106, } diff --git a/worlds/aquaria/Options.py b/worlds/aquaria/Options.py index 9a49e915b9cd..4c795d350898 100644 --- a/worlds/aquaria/Options.py +++ b/worlds/aquaria/Options.py @@ -113,7 +113,7 @@ class BindSongNeededToGetUnderRockBulb(Toggle): class UnconfineHomeWater(Choice): """ - Open the way out of the Home water area so that Naija can go to open water and beyond without the bind song. + Open the way out of the Home Water area so that Naija can go to open water and beyond without the bind song. """ display_name = "Unconfine Home Water Area" option_off = 0 diff --git a/worlds/aquaria/Regions.py b/worlds/aquaria/Regions.py index 5956e0ca842a..f2f85749f3fb 100755 --- a/worlds/aquaria/Regions.py +++ b/worlds/aquaria/Regions.py @@ -36,8 +36,8 @@ def _has_li(state:CollectionState, player: int) -> bool: def _has_damaging_item(state:CollectionState, player: int) -> bool: """`player` in `state` has the shield song item""" - return state.has_any({"Energy form", "Nature form", "Beast form", "Li and Li song", "Baby nautilus", - "Baby piranha", "Baby blaster"}, player) + return state.has_any({"Energy form", "Nature form", "Beast form", "Li and Li song", "Baby Nautilus", + "Baby Piranha", "Baby Blaster"}, player) def _has_shield_song(state:CollectionState, player: int) -> bool: @@ -72,7 +72,7 @@ def _has_sun_form(state:CollectionState, player: int) -> bool: def _has_light(state:CollectionState, player: int) -> bool: """`player` in `state` has the light item""" - return state.has("Baby dumbo", player) or _has_sun_form(state, player) + return state.has("Baby Dumbo", player) or _has_sun_form(state, player) def _has_dual_form(state:CollectionState, player: int) -> bool: @@ -237,26 +237,26 @@ def __create_home_water_area(self) -> None: AquariaLocations.locations_home_water_nautilus) self.home_water_transturtle = self.__add_region("Home Water, turtle room", AquariaLocations.locations_home_water_transturtle) - self.naija_home = self.__add_region("Naija's home", AquariaLocations.locations_naija_home) - self.song_cave = self.__add_region("Song cave", AquariaLocations.locations_song_cave) + self.naija_home = self.__add_region("Naija's Home", AquariaLocations.locations_naija_home) + self.song_cave = self.__add_region("Song Cave", AquariaLocations.locations_song_cave) def __create_energy_temple(self) -> None: """ Create the `energy_temple_*` regions """ - self.energy_temple_1 = self.__add_region("Energy temple first area", + self.energy_temple_1 = self.__add_region("Energy Temple first area", AquariaLocations.locations_energy_temple_1) - self.energy_temple_2 = self.__add_region("Energy temple second area", + self.energy_temple_2 = self.__add_region("Energy Temple second area", AquariaLocations.locations_energy_temple_2) - self.energy_temple_3 = self.__add_region("Energy temple third area", + self.energy_temple_3 = self.__add_region("Energy Temple third area", AquariaLocations.locations_energy_temple_3) - self.energy_temple_altar = self.__add_region("Energy temple bottom entrance", + self.energy_temple_altar = self.__add_region("Energy Temple bottom entrance", AquariaLocations.locations_energy_temple_altar) - self.energy_temple_boss = self.__add_region("Energy temple fallen God room", + self.energy_temple_boss = self.__add_region("Energy Temple fallen God room", AquariaLocations.locations_energy_temple_boss) - self.energy_temple_idol = self.__add_region("Energy temple Idol room", + self.energy_temple_idol = self.__add_region("Energy Temple Idol room", AquariaLocations.locations_energy_temple_idol) - self.energy_temple_blaster_room = self.__add_region("Energy temple blaster room", + self.energy_temple_blaster_room = self.__add_region("Energy Temple blaster room", AquariaLocations.locations_energy_temple_blaster_room) def __create_openwater(self) -> None: @@ -264,18 +264,18 @@ def __create_openwater(self) -> None: Create the `openwater_*`, `skeleton_path`, `arnassi*` and `simon` regions """ - self.openwater_tl = self.__add_region("Open water top left area", + self.openwater_tl = self.__add_region("Open Water top left area", AquariaLocations.locations_openwater_tl) - self.openwater_tr = self.__add_region("Open water top right area", + self.openwater_tr = self.__add_region("Open Water top right area", AquariaLocations.locations_openwater_tr) - self.openwater_tr_turtle = self.__add_region("Open water top right area, turtle room", + self.openwater_tr_turtle = self.__add_region("Open Water top right area, turtle room", AquariaLocations.locations_openwater_tr_turtle) - self.openwater_bl = self.__add_region("Open water bottom left area", + self.openwater_bl = self.__add_region("Open Water bottom left area", AquariaLocations.locations_openwater_bl) - self.openwater_br = self.__add_region("Open water bottom right area", None) - self.skeleton_path = self.__add_region("Open water skeleton path", + self.openwater_br = self.__add_region("Open Water bottom right area", None) + self.skeleton_path = self.__add_region("Open Water skeleton path", AquariaLocations.locations_skeleton_path) - self.skeleton_path_sc = self.__add_region("Open water skeleton path spirit cristal", + self.skeleton_path_sc = self.__add_region("Open Water skeleton path spirit crystal", AquariaLocations.locations_skeleton_path_sc) self.arnassi = self.__add_region("Arnassi Ruins", AquariaLocations.locations_arnassi) self.arnassi_path = self.__add_region("Arnassi Ruins, back entrance path", @@ -287,20 +287,20 @@ def __create_mithalas(self) -> None: """ Create the `mithalas_city*` and `cathedral_*` regions """ - self.mithalas_city = self.__add_region("Mithalas city", + self.mithalas_city = self.__add_region("Mithalas City", AquariaLocations.locations_mithalas_city) - self.mithalas_city_fishpass = self.__add_region("Mithalas city fish pass", + self.mithalas_city_fishpass = self.__add_region("Mithalas City fish pass", AquariaLocations.locations_mithalas_city_fishpass) - self.mithalas_city_top_path = self.__add_region("Mithalas city top path", + self.mithalas_city_top_path = self.__add_region("Mithalas City top path", AquariaLocations.locations_mithalas_city_top_path) self.cathedral_l = self.__add_region("Mithalas castle", AquariaLocations.locations_cathedral_l) self.cathedral_l_tube = self.__add_region("Mithalas castle, plant tube entrance", AquariaLocations.locations_cathedral_l_tube) - self.cathedral_l_sc = self.__add_region("Mithalas castle spirit cristal", + self.cathedral_l_sc = self.__add_region("Mithalas castle spirit crystal", AquariaLocations.locations_cathedral_l_sc) self.cathedral_r = self.__add_region("Mithalas Cathedral", AquariaLocations.locations_cathedral_r) - self.cathedral_underground = self.__add_region("Mithalas Cathedral underground area", + self.cathedral_underground = self.__add_region("Mithalas Cathedral Underground area", AquariaLocations.locations_cathedral_underground) self.cathedral_boss_r = self.__add_region("Mithalas Cathedral, Mithalan God room", AquariaLocations.locations_cathedral_boss) @@ -310,73 +310,73 @@ def __create_forest(self) -> None: """ Create the `forest_*` dans `mermog_cave` regions """ - self.forest_tl = self.__add_region("Kelp forest top left area", + self.forest_tl = self.__add_region("Kelp Forest top left area", AquariaLocations.locations_forest_tl) - self.forest_tl_fp = self.__add_region("Kelp forest top left area fish pass", + self.forest_tl_fp = self.__add_region("Kelp Forest top left area fish pass", AquariaLocations.locations_forest_tl_fp) - self.forest_tr = self.__add_region("Kelp forest top right area", + self.forest_tr = self.__add_region("Kelp Forest top right area", AquariaLocations.locations_forest_tr) - self.forest_tr_fp = self.__add_region("Kelp forest top right area fish pass", + self.forest_tr_fp = self.__add_region("Kelp Forest top right area fish pass", AquariaLocations.locations_forest_tr_fp) - self.forest_bl = self.__add_region("Kelp forest bottom left area", + self.forest_bl = self.__add_region("Kelp Forest bottom left area", AquariaLocations.locations_forest_bl) - self.forest_br = self.__add_region("Kelp forest bottom right area", + self.forest_br = self.__add_region("Kelp Forest bottom right area", AquariaLocations.locations_forest_br) - self.forest_sprite_cave = self.__add_region("Kelp forest spirit cave", + self.forest_sprite_cave = self.__add_region("Kelp Forest spirit cave", AquariaLocations.locations_forest_sprite_cave) - self.forest_sprite_cave_tube = self.__add_region("Kelp forest spirit cave after the plant tube", + self.forest_sprite_cave_tube = self.__add_region("Kelp Forest spirit cave after the plant tube", AquariaLocations.locations_forest_sprite_cave_tube) - self.forest_boss = self.__add_region("Kelp forest Drunian God room", + self.forest_boss = self.__add_region("Kelp Forest Drunian God room", AquariaLocations.locations_forest_boss) - self.forest_boss_entrance = self.__add_region("Kelp forest Drunian God room entrance", + self.forest_boss_entrance = self.__add_region("Kelp Forest Drunian God room entrance", AquariaLocations.locations_forest_boss_entrance) - self.mermog_cave = self.__add_region("Kelp forest Mermog cave", + self.mermog_cave = self.__add_region("Kelp Forest Mermog cave", AquariaLocations.locations_mermog_cave) - self.mermog_boss = self.__add_region("Kelp forest Mermog cave boss", + self.mermog_boss = self.__add_region("Kelp Forest Mermog cave boss", AquariaLocations.locations_mermog_boss) - self.forest_fish_cave = self.__add_region("Kelp forest fish cave", + self.forest_fish_cave = self.__add_region("Kelp Forest fish cave", AquariaLocations.locations_forest_fish_cave) - self.simon = self.__add_region("Kelp forest, Simon's room", AquariaLocations.locations_simon) + self.simon = self.__add_region("Kelp Forest, Simon's room", AquariaLocations.locations_simon) def __create_veil(self) -> None: """ Create the `veil_*`, `octo_cave` and `turtle_cave` regions """ - self.veil_tl = self.__add_region("The veil top left area", AquariaLocations.locations_veil_tl) - self.veil_tl_fp = self.__add_region("The veil top left area fish pass", + self.veil_tl = self.__add_region("The Veil top left area", AquariaLocations.locations_veil_tl) + self.veil_tl_fp = self.__add_region("The Veil top left area fish pass", AquariaLocations.locations_veil_tl_fp) - self.turtle_cave = self.__add_region("The veil top left area, turtle cave", + self.turtle_cave = self.__add_region("The Veil top left area, turtle cave", AquariaLocations.locations_turtle_cave) - self.turtle_cave_bubble = self.__add_region("The veil top left area, turtle cave bubble cliff", + self.turtle_cave_bubble = self.__add_region("The Veil top left area, turtle cave Bubble Cliff", AquariaLocations.locations_turtle_cave_bubble) - self.veil_tr_l = self.__add_region("The veil top right area, left of temple", + self.veil_tr_l = self.__add_region("The Veil top right area, left of temple", AquariaLocations.locations_veil_tr_l) - self.veil_tr_r = self.__add_region("The veil top right area, right of temple", + self.veil_tr_r = self.__add_region("The Veil top right area, right of temple", AquariaLocations.locations_veil_tr_r) - self.octo_cave_t = self.__add_region("Octopus cave top entrance", + self.octo_cave_t = self.__add_region("Octopus Cave top entrance", AquariaLocations.locations_octo_cave_t) - self.octo_cave_b = self.__add_region("Octopus cave bottom entrance", + self.octo_cave_b = self.__add_region("Octopus Cave bottom entrance", AquariaLocations.locations_octo_cave_b) - self.veil_bl = self.__add_region("The veil bottom left area", + self.veil_bl = self.__add_region("The Veil bottom left area", AquariaLocations.locations_veil_bl) - self.veil_b_sc = self.__add_region("The veil bottom spirit cristal area", + self.veil_b_sc = self.__add_region("The Veil bottom spirit crystal area", AquariaLocations.locations_veil_b_sc) - self.veil_bl_fp = self.__add_region("The veil bottom left area, in the sunken ship", + self.veil_bl_fp = self.__add_region("The Veil bottom left area, in the sunken ship", AquariaLocations.locations_veil_bl_fp) - self.veil_br = self.__add_region("The veil bottom right area", + self.veil_br = self.__add_region("The Veil bottom right area", AquariaLocations.locations_veil_br) def __create_sun_temple(self) -> None: """ Create the `sun_temple*` regions """ - self.sun_temple_l = self.__add_region("Sun temple left area", + self.sun_temple_l = self.__add_region("Sun Temple left area", AquariaLocations.locations_sun_temple_l) - self.sun_temple_r = self.__add_region("Sun temple right area", + self.sun_temple_r = self.__add_region("Sun Temple right area", AquariaLocations.locations_sun_temple_r) - self.sun_temple_boss_path = self.__add_region("Sun temple before boss area", + self.sun_temple_boss_path = self.__add_region("Sun Temple before boss area", AquariaLocations.locations_sun_temple_boss_path) - self.sun_temple_boss = self.__add_region("Sun temple boss area", + self.sun_temple_boss = self.__add_region("Sun Temple boss area", AquariaLocations.locations_sun_temple_boss) def __create_abyss(self) -> None: @@ -388,9 +388,9 @@ def __create_abyss(self) -> None: AquariaLocations.locations_abyss_l) self.abyss_lb = self.__add_region("Abyss left bottom area", AquariaLocations.locations_abyss_lb) self.abyss_r = self.__add_region("Abyss right area", AquariaLocations.locations_abyss_r) - self.ice_cave = self.__add_region("Ice cave", AquariaLocations.locations_ice_cave) - self.bubble_cave = self.__add_region("Bubble cave", AquariaLocations.locations_bubble_cave) - self.bubble_cave_boss = self.__add_region("Bubble cave boss area", AquariaLocations.locations_bubble_cave_boss) + self.ice_cave = self.__add_region("Ice Cave", AquariaLocations.locations_ice_cave) + self.bubble_cave = self.__add_region("Bubble Cave", AquariaLocations.locations_bubble_cave) + self.bubble_cave_boss = self.__add_region("Bubble Cave boss area", AquariaLocations.locations_bubble_cave_boss) self.king_jellyfish_cave = self.__add_region("Abyss left area, King jellyfish cave", AquariaLocations.locations_king_jellyfish_cave) self.whale = self.__add_region("Inside the whale", AquariaLocations.locations_whale) @@ -400,35 +400,35 @@ def __create_sunken_city(self) -> None: """ Create the `sunken_city_*` regions """ - self.sunken_city_l = self.__add_region("Sunken city left area", + self.sunken_city_l = self.__add_region("Sunken City left area", AquariaLocations.locations_sunken_city_l) - self.sunken_city_l_bedroom = self.__add_region("Sunken city left area, bedroom", + self.sunken_city_l_bedroom = self.__add_region("Sunken City left area, bedroom", AquariaLocations.locations_sunken_city_l_bedroom) - self.sunken_city_r = self.__add_region("Sunken city right area", + self.sunken_city_r = self.__add_region("Sunken City right area", AquariaLocations.locations_sunken_city_r) - self.sunken_city_boss = self.__add_region("Sunken city boss area", + self.sunken_city_boss = self.__add_region("Sunken City boss area", AquariaLocations.locations_sunken_city_boss) def __create_body(self) -> None: """ Create the `body_*` and `final_boss* regions """ - self.body_c = self.__add_region("The body center area", + self.body_c = self.__add_region("The Body center area", AquariaLocations.locations_body_c) - self.body_l = self.__add_region("The body left area", + self.body_l = self.__add_region("The Body left area", AquariaLocations.locations_body_l) - self.body_rt = self.__add_region("The body right area, top path", + self.body_rt = self.__add_region("The Body right area, top path", AquariaLocations.locations_body_rt) - self.body_rb = self.__add_region("The body right area, bottom path", + self.body_rb = self.__add_region("The Body right area, bottom path", AquariaLocations.locations_body_rb) - self.body_b = self.__add_region("The body bottom area", + self.body_b = self.__add_region("The Body bottom area", AquariaLocations.locations_body_b) - self.final_boss_loby = self.__add_region("The body, before final boss", None) - self.final_boss_tube = self.__add_region("The body, final boss area turtle room", + self.final_boss_loby = self.__add_region("The Body, before final boss", None) + self.final_boss_tube = self.__add_region("The Body, final boss area turtle room", AquariaLocations.locations_final_boss_tube) - self.final_boss = self.__add_region("The body, final boss", + self.final_boss = self.__add_region("The Body, final boss", AquariaLocations.locations_final_boss) - self.final_boss_end = self.__add_region("The body, final boss area", None) + self.final_boss_end = self.__add_region("The Body, final boss area", None) def __connect_one_way_regions(self, source_name: str, destination_name: str, source_region: Region, @@ -455,99 +455,99 @@ def __connect_home_water_regions(self) -> None: """ Connect entrances of the different regions around `home_water` """ - self.__connect_regions("Menu", "Verse cave right area", + self.__connect_regions("Menu", "Verse Cave right area", self.menu, self.verse_cave_r) - self.__connect_regions("Verse cave left area", "Verse cave right area", + self.__connect_regions("Verse Cave left area", "Verse Cave right area", self.verse_cave_l, self.verse_cave_r) - self.__connect_regions("Verse cave", "Home water", self.verse_cave_l, self.home_water) + self.__connect_regions("Verse Cave", "Home Water", self.verse_cave_l, self.home_water) self.__connect_regions("Home Water", "Haija's home", self.home_water, self.naija_home) - self.__connect_regions("Home Water", "Song cave", self.home_water, self.song_cave) - self.__connect_regions("Home Water", "Home water, nautilus nest", + self.__connect_regions("Home Water", "Song Cave", self.home_water, self.song_cave) + self.__connect_regions("Home Water", "Home Water, nautilus nest", self.home_water, self.home_water_nautilus, lambda state: _has_energy_form(state, self.player) and _has_bind_song(state, self.player)) - self.__connect_regions("Home Water", "Home water transturtle room", + self.__connect_regions("Home Water", "Home Water transturtle room", self.home_water, self.home_water_transturtle) - self.__connect_regions("Home Water", "Energy temple first area", + self.__connect_regions("Home Water", "Energy Temple first area", self.home_water, self.energy_temple_1, lambda state: _has_bind_song(state, self.player)) - self.__connect_regions("Home Water", "Energy temple_altar", + self.__connect_regions("Home Water", "Energy Temple_altar", self.home_water, self.energy_temple_altar, lambda state: _has_energy_form(state, self.player) and _has_bind_song(state, self.player)) - self.__connect_regions("Energy temple first area", "Energy temple second area", + self.__connect_regions("Energy Temple first area", "Energy Temple second area", self.energy_temple_1, self.energy_temple_2, lambda state: _has_energy_form(state, self.player)) - self.__connect_regions("Energy temple first area", "Energy temple idol room", + self.__connect_regions("Energy Temple first area", "Energy Temple idol room", self.energy_temple_1, self.energy_temple_idol, lambda state: _has_fish_form(state, self.player)) - self.__connect_regions("Energy temple idol room", "Energy temple boss area", + self.__connect_regions("Energy Temple idol room", "Energy Temple boss area", self.energy_temple_idol, self.energy_temple_boss, lambda state: _has_energy_form(state, self.player)) - self.__connect_one_way_regions("Energy temple first area", "Energy temple boss area", + self.__connect_one_way_regions("Energy Temple first area", "Energy Temple boss area", self.energy_temple_1, self.energy_temple_boss, lambda state: _has_beast_form(state, self.player) and _has_energy_form(state, self.player)) - self.__connect_one_way_regions("Energy temple boss area", "Energy temple first area", + self.__connect_one_way_regions("Energy Temple boss area", "Energy Temple first area", self.energy_temple_boss, self.energy_temple_1, lambda state: _has_energy_form(state, self.player)) - self.__connect_regions("Energy temple second area", "Energy temple third area", + self.__connect_regions("Energy Temple second area", "Energy Temple third area", self.energy_temple_2, self.energy_temple_3, lambda state: _has_bind_song(state, self.player) and _has_energy_form(state, self.player)) - self.__connect_regions("Energy temple boss area", "Energy temple blaster room", + self.__connect_regions("Energy Temple boss area", "Energy Temple blaster room", self.energy_temple_boss, self.energy_temple_blaster_room, lambda state: _has_nature_form(state, self.player) and _has_bind_song(state, self.player) and _has_energy_form(state, self.player)) - self.__connect_regions("Energy temple first area", "Energy temple blaster room", + self.__connect_regions("Energy Temple first area", "Energy Temple blaster room", self.energy_temple_1, self.energy_temple_blaster_room, lambda state: _has_nature_form(state, self.player) and _has_bind_song(state, self.player) and _has_energy_form(state, self.player) and _has_beast_form(state, self.player)) - self.__connect_regions("Home Water", "Open water top left area", + self.__connect_regions("Home Water", "Open Water top left area", self.home_water, self.openwater_tl) def __connect_open_water_regions(self) -> None: """ Connect entrances of the different regions around open water """ - self.__connect_regions("Open water top left area", "Open water top right area", + self.__connect_regions("Open Water top left area", "Open Water top right area", self.openwater_tl, self.openwater_tr) - self.__connect_regions("Open water top left area", "Open water bottom left area", + self.__connect_regions("Open Water top left area", "Open Water bottom left area", self.openwater_tl, self.openwater_bl) - self.__connect_regions("Open water top left area", "forest bottom right area", + self.__connect_regions("Open Water top left area", "forest bottom right area", self.openwater_tl, self.forest_br) - self.__connect_regions("Open water top right area", "Open water top right area, turtle room", + self.__connect_regions("Open Water top right area", "Open Water top right area, turtle room", self.openwater_tr, self.openwater_tr_turtle, lambda state: _has_beast_form(state, self.player)) - self.__connect_regions("Open water top right area", "Open water bottom right area", + self.__connect_regions("Open Water top right area", "Open Water bottom right area", self.openwater_tr, self.openwater_br) - self.__connect_regions("Open water top right area", "Mithalas city", + self.__connect_regions("Open Water top right area", "Mithalas City", self.openwater_tr, self.mithalas_city) - self.__connect_regions("Open water top right area", "Veil bottom left area", + self.__connect_regions("Open Water top right area", "Veil bottom left area", self.openwater_tr, self.veil_bl) - self.__connect_one_way_regions("Open water top right area", "Veil bottom right", + self.__connect_one_way_regions("Open Water top right area", "Veil bottom right", self.openwater_tr, self.veil_br, lambda state: _has_beast_form(state, self.player)) - self.__connect_one_way_regions("Veil bottom right", "Open water top right area", + self.__connect_one_way_regions("Veil bottom right", "Open Water top right area", self.veil_br, self.openwater_tr, lambda state: _has_beast_form(state, self.player)) - self.__connect_regions("Open water bottom left area", "Open water bottom right area", + self.__connect_regions("Open Water bottom left area", "Open Water bottom right area", self.openwater_bl, self.openwater_br) - self.__connect_regions("Open water bottom left area", "Skeleton path", + self.__connect_regions("Open Water bottom left area", "Skeleton path", self.openwater_bl, self.skeleton_path) - self.__connect_regions("Abyss left area", "Open water bottom left area", + self.__connect_regions("Abyss left area", "Open Water bottom left area", self.abyss_l, self.openwater_bl) self.__connect_regions("Skeleton path", "skeleton_path_sc", self.skeleton_path, self.skeleton_path_sc, lambda state: _has_spirit_form(state, self.player)) - self.__connect_regions("Abyss right area", "Open water bottom right area", + self.__connect_regions("Abyss right area", "Open Water bottom right area", self.abyss_r, self.openwater_br) - self.__connect_one_way_regions("Open water bottom right area", "Arnassi", + self.__connect_one_way_regions("Open Water bottom right area", "Arnassi", self.openwater_br, self.arnassi, lambda state: _has_beast_form(state, self.player)) - self.__connect_one_way_regions("Arnassi", "Open water bottom right area", + self.__connect_one_way_regions("Arnassi", "Open Water bottom right area", self.arnassi, self.openwater_br) self.__connect_regions("Arnassi", "Arnassi path", self.arnassi, self.arnassi_path) @@ -562,23 +562,23 @@ def __connect_mithalas_regions(self) -> None: """ Connect entrances of the different regions around Mithalas """ - self.__connect_one_way_regions("Mithalas city", "Mithalas city top path", + self.__connect_one_way_regions("Mithalas City", "Mithalas City top path", self.mithalas_city, self.mithalas_city_top_path, lambda state: _has_beast_form(state, self.player)) - self.__connect_one_way_regions("Mithalas city_top_path", "Mithalas city", + self.__connect_one_way_regions("Mithalas City_top_path", "Mithalas City", self.mithalas_city_top_path, self.mithalas_city) - self.__connect_regions("Mithalas city", "Mithalas city home with fishpass", + self.__connect_regions("Mithalas City", "Mithalas City home with fishpass", self.mithalas_city, self.mithalas_city_fishpass, lambda state: _has_fish_form(state, self.player)) - self.__connect_regions("Mithalas city", "Mithalas castle", + self.__connect_regions("Mithalas City", "Mithalas castle", self.mithalas_city, self.cathedral_l, lambda state: _has_fish_form(state, self.player)) - self.__connect_one_way_regions("Mithalas city top path", "Mithalas castle, flower tube", + self.__connect_one_way_regions("Mithalas City top path", "Mithalas castle, flower tube", self.mithalas_city_top_path, self.cathedral_l_tube, lambda state: _has_nature_form(state, self.player) and _has_energy_form(state, self.player)) - self.__connect_one_way_regions("Mithalas castle, flower tube area", "Mithalas city top path", + self.__connect_one_way_regions("Mithalas castle, flower tube area", "Mithalas City top path", self.cathedral_l_tube, self.mithalas_city_top_path, lambda state: _has_beast_form(state, self.player) and @@ -690,22 +690,22 @@ def __connect_veil_regions(self) -> None: self.veil_tl, self.veil_tr_r) self.__connect_regions("Veil top left area", "Turtle cave", self.veil_tl, self.turtle_cave) - self.__connect_regions("Turtle cave", "Turtle cave bubble cliff", + self.__connect_regions("Turtle cave", "Turtle cave Bubble Cliff", self.turtle_cave, self.turtle_cave_bubble, lambda state: _has_beast_form(state, self.player)) - self.__connect_regions("Veil right of sun temple", "Sun temple right area", + self.__connect_regions("Veil right of sun temple", "Sun Temple right area", self.veil_tr_r, self.sun_temple_r) - self.__connect_regions("Sun temple right area", "Sun temple left area", + self.__connect_regions("Sun Temple right area", "Sun Temple left area", self.sun_temple_r, self.sun_temple_l, lambda state: _has_bind_song(state, self.player)) - self.__connect_regions("Sun temple left area", "Veil left of sun temple", + self.__connect_regions("Sun Temple left area", "Veil left of sun temple", self.sun_temple_l, self.veil_tr_l) - self.__connect_regions("Sun temple left area", "Sun temple before boss area", + self.__connect_regions("Sun Temple left area", "Sun Temple before boss area", self.sun_temple_l, self.sun_temple_boss_path) - self.__connect_regions("Sun temple before boss area", "Sun temple boss area", + self.__connect_regions("Sun Temple before boss area", "Sun Temple boss area", self.sun_temple_boss_path, self.sun_temple_boss, lambda state: _has_energy_form(state, self.player)) - self.__connect_one_way_regions("Sun temple boss area", "Veil left of sun temple", + self.__connect_one_way_regions("Sun Temple boss area", "Veil left of sun temple", self.sun_temple_boss, self.veil_tr_l) self.__connect_regions("Veil left of sun temple", "Octo cave top path", self.veil_tr_l, self.octo_cave_t, @@ -724,7 +724,7 @@ def __connect_abyss_regions(self) -> None: self.__connect_regions("Abyss left area", "Abyss bottom of left area", self.abyss_l, self.abyss_lb, lambda state: _has_nature_form(state, self.player)) - self.__connect_regions("Abyss left bottom area", "Sunken city right area", + self.__connect_regions("Abyss left bottom area", "Sunken City right area", self.abyss_lb, self.sunken_city_r, lambda state: _has_li(state, self.player)) self.__connect_one_way_regions("Abyss left bottom area", "Body center area", @@ -748,13 +748,13 @@ def __connect_abyss_regions(self) -> None: _has_sun_form(state, self.player) and _has_bind_song(state, self.player) and _has_energy_form(state, self.player)) - self.__connect_regions("Abyss right area", "Ice cave", + self.__connect_regions("Abyss right area", "Ice Cave", self.abyss_r, self.ice_cave, lambda state: _has_spirit_form(state, self.player)) - self.__connect_regions("Abyss right area", "Bubble cave", + self.__connect_regions("Abyss right area", "Bubble Cave", self.ice_cave, self.bubble_cave, lambda state: _has_beast_form(state, self.player)) - self.__connect_regions("Bubble cave boss area", "Bubble cave", + self.__connect_regions("Bubble Cave boss area", "Bubble Cave", self.bubble_cave, self.bubble_cave_boss, lambda state: _has_nature_form(state, self.player) and _has_bind_song(state, self.player) ) @@ -763,12 +763,12 @@ def __connect_sunken_city_regions(self) -> None: """ Connect entrances of the different regions around The Sunken City """ - self.__connect_regions("Sunken city right area", "Sunken city left area", + self.__connect_regions("Sunken City right area", "Sunken City left area", self.sunken_city_r, self.sunken_city_l) - self.__connect_regions("Sunken city left area", "Sunken city bedroom", + self.__connect_regions("Sunken City left area", "Sunken City bedroom", self.sunken_city_l, self.sunken_city_l_bedroom, lambda state: _has_spirit_form(state, self.player)) - self.__connect_regions("Sunken city left area", "Sunken city boss area", + self.__connect_regions("Sunken City left area", "Sunken City boss area", self.sunken_city_l, self.sunken_city_boss, lambda state: _has_beast_form(state, self.player) and _has_energy_form(state, self.player) and @@ -776,7 +776,7 @@ def __connect_sunken_city_regions(self) -> None: def __connect_body_regions(self) -> None: """ - Connect entrances of the different regions around The body + Connect entrances of the different regions around The Body """ self.__connect_regions("Body center area", "Body left area", self.body_c, self.body_l) @@ -787,13 +787,13 @@ def __connect_body_regions(self) -> None: self.__connect_regions("Body center area", "Body bottom area", self.body_c, self.body_b, lambda state: _has_dual_form(state, self.player)) - self.__connect_regions("Body bottom area", "Final boss area", + self.__connect_regions("Body bottom area", "Final Boss area", self.body_b, self.final_boss_loby, lambda state: _has_dual_form(state, self.player)) - self.__connect_regions("Before Final boss", "Final boss tube", + self.__connect_regions("Before Final Boss", "Final Boss tube", self.final_boss_loby, self.final_boss_tube, lambda state: _has_nature_form(state, self.player)) - self.__connect_one_way_regions("Before Final boss", "Final boss", + self.__connect_one_way_regions("Before Final Boss", "Final Boss", self.final_boss_loby, self.final_boss, lambda state: _has_energy_form(state, self.player) and _has_dual_form(state, self.player) and @@ -814,7 +814,7 @@ def __connect_transturtle(self, item_source: str, item_target: str, region_sourc def __connect_arnassi_path_transturtle(self, item_source: str, item_target: str, region_source: Region, region_target: Region) -> None: - """Connect the Arnassi ruins transturtle to another one""" + """Connect the Arnassi Ruins transturtle to another one""" self.__connect_one_way_regions(item_source, item_target, region_source, region_target, lambda state: state.has(item_target, self.player) and _has_fish_form(state, self.player)) @@ -825,25 +825,25 @@ def _connect_transturtle_to_other(self, item: str, region: Region) -> None: self.__connect_transturtle(item, "Transturtle Veil top right", region, self.veil_tr_l) self.__connect_transturtle(item, "Transturtle Open Water top right", region, self.openwater_tr_turtle) self.__connect_transturtle(item, "Transturtle Forest bottom left", region, self.forest_bl) - self.__connect_transturtle(item, "Transturtle Home water", region, self.home_water_transturtle) + self.__connect_transturtle(item, "Transturtle Home Water", region, self.home_water_transturtle) self.__connect_transturtle(item, "Transturtle Abyss right", region, self.abyss_r) self.__connect_transturtle(item, "Transturtle Final Boss", region, self.final_boss_tube) - self.__connect_transturtle(item, "Transturtle Simon says", region, self.simon) - self.__connect_transturtle(item, "Transturtle Arnassi ruins", region, self.arnassi_path, - lambda state: state.has("Transturtle Arnassi ruins", self.player) and + self.__connect_transturtle(item, "Transturtle Simon Says", region, self.simon) + self.__connect_transturtle(item, "Transturtle Arnassi Ruins", region, self.arnassi_path, + lambda state: state.has("Transturtle Arnassi Ruins", self.player) and _has_fish_form(state, self.player)) def _connect_arnassi_path_transturtle_to_other(self, item: str, region: Region) -> None: - """Connect the Arnassi ruins transturtle to all others""" + """Connect the Arnassi Ruins transturtle to all others""" self.__connect_arnassi_path_transturtle(item, "Transturtle Veil top left", region, self.veil_tl) self.__connect_arnassi_path_transturtle(item, "Transturtle Veil top right", region, self.veil_tr_l) self.__connect_arnassi_path_transturtle(item, "Transturtle Open Water top right", region, self.openwater_tr_turtle) self.__connect_arnassi_path_transturtle(item, "Transturtle Forest bottom left", region, self.forest_bl) - self.__connect_arnassi_path_transturtle(item, "Transturtle Home water", region, self.home_water_transturtle) + self.__connect_arnassi_path_transturtle(item, "Transturtle Home Water", region, self.home_water_transturtle) self.__connect_arnassi_path_transturtle(item, "Transturtle Abyss right", region, self.abyss_r) self.__connect_arnassi_path_transturtle(item, "Transturtle Final Boss", region, self.final_boss_tube) - self.__connect_arnassi_path_transturtle(item, "Transturtle Simon says", region, self.simon) + self.__connect_arnassi_path_transturtle(item, "Transturtle Simon Says", region, self.simon) def __connect_transturtles(self) -> None: """Connect every transturtle with others""" @@ -851,11 +851,11 @@ def __connect_transturtles(self) -> None: self._connect_transturtle_to_other("Transturtle Veil top right", self.veil_tr_l) self._connect_transturtle_to_other("Transturtle Open Water top right", self.openwater_tr_turtle) self._connect_transturtle_to_other("Transturtle Forest bottom left", self.forest_bl) - self._connect_transturtle_to_other("Transturtle Home water", self.home_water_transturtle) + self._connect_transturtle_to_other("Transturtle Home Water", self.home_water_transturtle) self._connect_transturtle_to_other("Transturtle Abyss right", self.abyss_r) self._connect_transturtle_to_other("Transturtle Final Boss", self.final_boss_tube) - self._connect_transturtle_to_other("Transturtle Simon says", self.simon) - self._connect_arnassi_path_transturtle_to_other("Transturtle Arnassi ruins", self.arnassi_path) + self._connect_transturtle_to_other("Transturtle Simon Says", self.simon) + self._connect_arnassi_path_transturtle_to_other("Transturtle Arnassi Ruins", self.arnassi_path) def connect_regions(self) -> None: """ @@ -907,7 +907,7 @@ def __add_event_big_bosses(self) -> None: def __add_event_mini_bosses(self) -> None: """ - Add every mini bosses (excluding Energy statue and Simon says) + Add every mini bosses (excluding Energy Statue and Simon Says) events to the `world` """ self.__add_event_location(self.home_water_nautilus, @@ -967,100 +967,100 @@ def add_event_locations(self) -> None: def __adjusting_urns_rules(self) -> None: """Since Urns need to be broken, add a damaging item to rules""" - add_rule(self.multiworld.get_location("Open water top right area, first urn in the Mithalas exit", self.player), + add_rule(self.multiworld.get_location("Open Water top right area, first urn in the Mithalas exit", self.player), lambda state: _has_damaging_item(state, self.player)) - add_rule(self.multiworld.get_location("Open water top right area, second urn in the Mithalas exit", self.player), + add_rule(self.multiworld.get_location("Open Water top right area, second urn in the Mithalas exit", self.player), lambda state: _has_damaging_item(state, self.player)) - add_rule(self.multiworld.get_location("Open water top right area, third urn in the Mithalas exit", self.player), + add_rule(self.multiworld.get_location("Open Water top right area, third urn in the Mithalas exit", self.player), lambda state: _has_damaging_item(state, self.player)) - add_rule(self.multiworld.get_location("Mithalas city, first urn in one of the homes", self.player), + add_rule(self.multiworld.get_location("Mithalas City, first urn in one of the homes", self.player), lambda state: _has_damaging_item(state, self.player)) - add_rule(self.multiworld.get_location("Mithalas city, second urn in one of the homes", self.player), + add_rule(self.multiworld.get_location("Mithalas City, second urn in one of the homes", self.player), lambda state: _has_damaging_item(state, self.player)) - add_rule(self.multiworld.get_location("Mithalas city, first urn in the city reserve", self.player), + add_rule(self.multiworld.get_location("Mithalas City, first urn in the city reserve", self.player), lambda state: _has_damaging_item(state, self.player)) - add_rule(self.multiworld.get_location("Mithalas city, second urn in the city reserve", self.player), + add_rule(self.multiworld.get_location("Mithalas City, second urn in the city reserve", self.player), lambda state: _has_damaging_item(state, self.player)) - add_rule(self.multiworld.get_location("Mithalas city, third urn in the city reserve", self.player), + add_rule(self.multiworld.get_location("Mithalas City, third urn in the city reserve", self.player), lambda state: _has_damaging_item(state, self.player)) - add_rule(self.multiworld.get_location("Mithalas city, urn in the cathedral flower tube entrance", self.player), + add_rule(self.multiworld.get_location("Mithalas City, urn in the Cathedral flower tube entrance", self.player), lambda state: _has_damaging_item(state, self.player)) - add_rule(self.multiworld.get_location("Mithalas city castle, urn in the bedroom", self.player), + add_rule(self.multiworld.get_location("Mithalas City Castle, urn in the bedroom", self.player), lambda state: _has_damaging_item(state, self.player)) - add_rule(self.multiworld.get_location("Mithalas city castle, first urn of the single lamp path", self.player), + add_rule(self.multiworld.get_location("Mithalas City Castle, first urn of the single lamp path", self.player), lambda state: _has_damaging_item(state, self.player)) - add_rule(self.multiworld.get_location("Mithalas city castle, second urn of the single lamp path", self.player), + add_rule(self.multiworld.get_location("Mithalas City Castle, second urn of the single lamp path", self.player), lambda state: _has_damaging_item(state, self.player)) - add_rule(self.multiworld.get_location("Mithalas city castle, urn in the bottom room", self.player), + add_rule(self.multiworld.get_location("Mithalas City Castle, urn in the bottom room", self.player), lambda state: _has_damaging_item(state, self.player)) - add_rule(self.multiworld.get_location("Mithalas city castle, first urn on the entrance path", self.player), + add_rule(self.multiworld.get_location("Mithalas City Castle, first urn on the entrance path", self.player), lambda state: _has_damaging_item(state, self.player)) - add_rule(self.multiworld.get_location("Mithalas city castle, second urn on the entrance path", self.player), + add_rule(self.multiworld.get_location("Mithalas City Castle, second urn on the entrance path", self.player), lambda state: _has_damaging_item(state, self.player)) - add_rule(self.multiworld.get_location("Mithalas city, urn inside a home fish pass", self.player), + add_rule(self.multiworld.get_location("Mithalas City, urn inside a home fish pass", self.player), lambda state: _has_damaging_item(state, self.player)) def __adjusting_crates_rules(self) -> None: """Since Crate need to be broken, add a damaging item to rules""" - add_rule(self.multiworld.get_location("Sunken city right area, crate close to the save cristal", self.player), + add_rule(self.multiworld.get_location("Sunken City right area, crate close to the save crystal", self.player), lambda state: _has_damaging_item(state, self.player)) - add_rule(self.multiworld.get_location("Sunken city right area, crate in the left bottom room", self.player), + add_rule(self.multiworld.get_location("Sunken City right area, crate in the left bottom room", self.player), lambda state: _has_damaging_item(state, self.player)) - add_rule(self.multiworld.get_location("Sunken city left area, crate in the little pipe room", self.player), + add_rule(self.multiworld.get_location("Sunken City left area, crate in the little pipe room", self.player), lambda state: _has_damaging_item(state, self.player)) - add_rule(self.multiworld.get_location("Sunken city left area, crate close to the save cristal", self.player), + add_rule(self.multiworld.get_location("Sunken City left area, crate close to the save crystal", self.player), lambda state: _has_damaging_item(state, self.player)) - add_rule(self.multiworld.get_location("Sunken city left area, crate before the bedroom", self.player), + add_rule(self.multiworld.get_location("Sunken City left area, crate before the bedroom", self.player), lambda state: _has_damaging_item(state, self.player)) def __adjusting_soup_rules(self) -> None: """ Modify rules for location that need soup """ - add_rule(self.multiworld.get_location("Turtle cave, Urchin costume", self.player), + add_rule(self.multiworld.get_location("Turtle cave, Urchin Costume", self.player), lambda state: _has_hot_soup(state, self.player) and _has_beast_form(state, self.player)) add_rule(self.multiworld.get_location("Sun Worm path, first cliff bulb", self.player), lambda state: _has_hot_soup(state, self.player) and _has_beast_form(state, self.player)) add_rule(self.multiworld.get_location("Sun Worm path, second cliff bulb", self.player), lambda state: _has_hot_soup(state, self.player) and _has_beast_form(state, self.player)) - add_rule(self.multiworld.get_location("The veil top right area, bulb in the top of the water fall", self.player), + add_rule(self.multiworld.get_location("The Veil top right area, bulb in the top of the waterfall", self.player), lambda state: _has_hot_soup(state, self.player) and _has_beast_form(state, self.player)) def __adjusting_under_rock_location(self) -> None: """ Modify rules implying bind song needed for bulb under rocks """ - add_rule(self.multiworld.get_location("Home water, bulb under the rock in the left path from the verse cave", + add_rule(self.multiworld.get_location("Home Water, bulb under the rock in the left path from the Verse Cave", self.player), lambda state: _has_bind_song(state, self.player)) - add_rule(self.multiworld.get_location("Verse cave left area, bulb under the rock at the end of the path", + add_rule(self.multiworld.get_location("Verse Cave left area, bulb under the rock at the end of the path", self.player), lambda state: _has_bind_song(state, self.player)) - add_rule(self.multiworld.get_location("Naija's home, bulb under the rock at the right of the main path", + add_rule(self.multiworld.get_location("Naija's Home, bulb under the rock at the right of the main path", self.player), lambda state: _has_bind_song(state, self.player)) - add_rule(self.multiworld.get_location("Song cave, bulb under the rock in the path to the singing statues", + add_rule(self.multiworld.get_location("Song Cave, bulb under the rock in the path to the singing statues", self.player), lambda state: _has_bind_song(state, self.player)) - add_rule(self.multiworld.get_location("Song cave, bulb under the rock close to the song door", + add_rule(self.multiworld.get_location("Song Cave, bulb under the rock close to the song door", self.player), lambda state: _has_bind_song(state, self.player)) - add_rule(self.multiworld.get_location("Energy temple second area, bulb under the rock", + add_rule(self.multiworld.get_location("Energy Temple second area, bulb under the rock", self.player), lambda state: _has_bind_song(state, self.player)) - add_rule(self.multiworld.get_location("Open water top left area, bulb under the rock in the right path", + add_rule(self.multiworld.get_location("Open Water top left area, bulb under the rock in the right path", self.player), lambda state: _has_bind_song(state, self.player)) - add_rule(self.multiworld.get_location("Open water top left area, bulb under the rock in the left path", + add_rule(self.multiworld.get_location("Open Water top left area, bulb under the rock in the left path", self.player), lambda state: _has_bind_song(state, self.player)) add_rule(self.multiworld.get_location("Kelp Forest top right area, bulb under the rock in the right path", self.player), lambda state: _has_bind_song(state, self.player)) - add_rule(self.multiworld.get_location("The veil top left area, bulb under the rock in the top right path", + add_rule(self.multiworld.get_location("The Veil top left area, bulb under the rock in the top right path", self.player), lambda state: _has_bind_song(state, self.player)) add_rule(self.multiworld.get_location("Abyss right area, bulb behind the rock in the whale room", self.player), lambda state: _has_bind_song(state, self.player)) add_rule(self.multiworld.get_location("Abyss right area, bulb in the middle path", self.player), lambda state: _has_bind_song(state, self.player)) - add_rule(self.multiworld.get_location("The veil top left area, bulb under the rock in the top right path", + add_rule(self.multiworld.get_location("The Veil top left area, bulb under the rock in the top right path", self.player), lambda state: _has_bind_song(state, self.player)) def __adjusting_light_in_dark_place_rules(self) -> None: - add_rule(self.multiworld.get_location("Kelp forest top right area, Black pearl", self.player), + add_rule(self.multiworld.get_location("Kelp Forest top right area, Black Pearl", self.player), lambda state: _has_light(state, self.player)) - add_rule(self.multiworld.get_location("Kelp forest bottom right area, Odd Container", self.player), + add_rule(self.multiworld.get_location("Kelp Forest bottom right area, Odd Container", self.player), lambda state: _has_light(state, self.player)) add_rule(self.multiworld.get_entrance("Transturtle Veil top left to Transturtle Abyss right", self.player), lambda state: _has_light(state, self.player)) @@ -1070,103 +1070,103 @@ def __adjusting_light_in_dark_place_rules(self) -> None: lambda state: _has_light(state, self.player)) add_rule(self.multiworld.get_entrance("Transturtle Forest bottom left to Transturtle Abyss right", self.player), lambda state: _has_light(state, self.player)) - add_rule(self.multiworld.get_entrance("Transturtle Home water to Transturtle Abyss right", self.player), + add_rule(self.multiworld.get_entrance("Transturtle Home Water to Transturtle Abyss right", self.player), lambda state: _has_light(state, self.player)) add_rule(self.multiworld.get_entrance("Transturtle Final Boss to Transturtle Abyss right", self.player), lambda state: _has_light(state, self.player)) - add_rule(self.multiworld.get_entrance("Transturtle Simon says to Transturtle Abyss right", self.player), + add_rule(self.multiworld.get_entrance("Transturtle Simon Says to Transturtle Abyss right", self.player), lambda state: _has_light(state, self.player)) - add_rule(self.multiworld.get_entrance("Transturtle Arnassi ruins to Transturtle Abyss right", self.player), + add_rule(self.multiworld.get_entrance("Transturtle Arnassi Ruins to Transturtle Abyss right", self.player), lambda state: _has_light(state, self.player)) add_rule(self.multiworld.get_entrance("Body center area to Abyss left bottom area", self.player), lambda state: _has_light(state, self.player)) add_rule(self.multiworld.get_entrance("Veil left of sun temple to Octo cave top path", self.player), lambda state: _has_light(state, self.player)) - add_rule(self.multiworld.get_entrance("Open water bottom right area to Abyss right area", self.player), + add_rule(self.multiworld.get_entrance("Open Water bottom right area to Abyss right area", self.player), lambda state: _has_light(state, self.player)) - add_rule(self.multiworld.get_entrance("Open water bottom left area to Abyss left area", self.player), + add_rule(self.multiworld.get_entrance("Open Water bottom left area to Abyss left area", self.player), lambda state: _has_light(state, self.player)) - add_rule(self.multiworld.get_entrance("Sun temple left area to Sun temple right area", self.player), + add_rule(self.multiworld.get_entrance("Sun Temple left area to Sun Temple right area", self.player), lambda state: _has_light(state, self.player) or _has_sun_crystal(state, self.player)) - add_rule(self.multiworld.get_entrance("Sun temple right area to Sun temple left area", self.player), + add_rule(self.multiworld.get_entrance("Sun Temple right area to Sun Temple left area", self.player), lambda state: _has_light(state, self.player) or _has_sun_crystal(state, self.player)) - add_rule(self.multiworld.get_entrance("Veil left of sun temple to Sun temple left area", self.player), + add_rule(self.multiworld.get_entrance("Veil left of sun temple to Sun Temple left area", self.player), lambda state: _has_light(state, self.player) or _has_sun_crystal(state, self.player)) def __adjusting_manual_rules(self) -> None: - add_rule(self.multiworld.get_location("Mithalas cathedral, Mithalan Dress", self.player), + add_rule(self.multiworld.get_location("Mithalas Cathedral, Mithalan Dress", self.player), lambda state: _has_beast_form(state, self.player)) - add_rule(self.multiworld.get_location("Open water bottom left area, bulb inside the lowest fish pass", self.player), + add_rule(self.multiworld.get_location("Open Water bottom left area, bulb inside the lowest fish pass", self.player), lambda state: _has_fish_form(state, self.player)) - add_rule(self.multiworld.get_location("Kelp forest bottom left area, Walker baby", self.player), + add_rule(self.multiworld.get_location("Kelp Forest bottom left area, Walker baby", self.player), lambda state: _has_spirit_form(state, self.player)) - add_rule(self.multiworld.get_location("The veil top left area, bulb hidden behind the blocking rock", self.player), + add_rule(self.multiworld.get_location("The Veil top left area, bulb hidden behind the blocking rock", self.player), lambda state: _has_bind_song(state, self.player)) add_rule(self.multiworld.get_location("Turtle cave, Turtle Egg", self.player), lambda state: _has_bind_song(state, self.player)) add_rule(self.multiworld.get_location("Abyss left area, bulb in the bottom fish pass", self.player), lambda state: _has_fish_form(state, self.player)) - add_rule(self.multiworld.get_location("Song cave, Anemone seed", self.player), + add_rule(self.multiworld.get_location("Song Cave, Anemone Seed", self.player), lambda state: _has_nature_form(state, self.player)) - add_rule(self.multiworld.get_location("Song cave, Verse egg", self.player), + add_rule(self.multiworld.get_location("Song Cave, Verse Egg", self.player), lambda state: _has_bind_song(state, self.player)) - add_rule(self.multiworld.get_location("Verse cave right area, Big Seed", self.player), + add_rule(self.multiworld.get_location("Verse Cave right area, Big Seed", self.player), lambda state: _has_bind_song(state, self.player)) - add_rule(self.multiworld.get_location("Arnassi ruins, Song plant spore on the top of the ruins", self.player), + add_rule(self.multiworld.get_location("Arnassi Ruins, Song Plant Spore", self.player), lambda state: _has_beast_form(state, self.player)) - add_rule(self.multiworld.get_location("Energy temple first area, bulb in the bottom room blocked by a rock", + add_rule(self.multiworld.get_location("Energy Temple first area, bulb in the bottom room blocked by a rock", self.player), lambda state: _has_energy_form(state, self.player)) - add_rule(self.multiworld.get_location("Home water, bulb in the bottom left room", self.player), + add_rule(self.multiworld.get_location("Home Water, bulb in the bottom left room", self.player), lambda state: _has_bind_song(state, self.player)) - add_rule(self.multiworld.get_location("Home water, bulb in the path below Nautilus Prime", self.player), + add_rule(self.multiworld.get_location("Home Water, bulb in the path below Nautilus Prime", self.player), lambda state: _has_bind_song(state, self.player)) - add_rule(self.multiworld.get_location("Naija's home, bulb after the energy door", self.player), + add_rule(self.multiworld.get_location("Naija's Home, bulb after the energy door", self.player), lambda state: _has_energy_form(state, self.player)) add_rule(self.multiworld.get_location("Abyss right area, bulb behind the rock in the whale room", self.player), lambda state: _has_spirit_form(state, self.player) and _has_sun_form(state, self.player)) - add_rule(self.multiworld.get_location("Arnassi ruins, Arnassi Armor", self.player), + add_rule(self.multiworld.get_location("Arnassi Ruins, Arnassi Armor", self.player), lambda state: _has_fish_form(state, self.player) and _has_spirit_form(state, self.player)) def __no_progression_hard_or_hidden_location(self) -> None: - self.multiworld.get_location("Energy temple boss area, Fallen god tooth", + self.multiworld.get_location("Energy Temple boss area, Fallen God Tooth", self.player).item_rule =\ lambda item: item.classification != ItemClassification.progression self.multiworld.get_location("Cathedral boss area, beating Mithalan God", self.player).item_rule =\ lambda item: item.classification != ItemClassification.progression - self.multiworld.get_location("Kelp forest boss area, beating Drunian God", + self.multiworld.get_location("Kelp Forest boss area, beating Drunian God", self.player).item_rule =\ lambda item: item.classification != ItemClassification.progression - self.multiworld.get_location("Sun temple boss area, beating Sun God", + self.multiworld.get_location("Sun Temple boss area, beating Sun God", self.player).item_rule =\ lambda item: item.classification != ItemClassification.progression - self.multiworld.get_location("Sunken city, bulb on the top of the boss area (boiler room)", + self.multiworld.get_location("Sunken City, bulb on top of the boss area", self.player).item_rule =\ lambda item: item.classification != ItemClassification.progression - self.multiworld.get_location("Home water, Nautilus Egg", + self.multiworld.get_location("Home Water, Nautilus Egg", self.player).item_rule =\ lambda item: item.classification != ItemClassification.progression - self.multiworld.get_location("Energy temple blaster room, Blaster egg", + self.multiworld.get_location("Energy Temple blaster room, Blaster Egg", self.player).item_rule =\ lambda item: item.classification != ItemClassification.progression - self.multiworld.get_location("Mithalas castle, beating the priests", + self.multiworld.get_location("Mithalas City Castle, beating the Priests", self.player).item_rule =\ lambda item: item.classification != ItemClassification.progression self.multiworld.get_location("Mermog cave, Piranha Egg", self.player).item_rule =\ lambda item: item.classification != ItemClassification.progression - self.multiworld.get_location("Octopus cave, Dumbo Egg", + self.multiworld.get_location("Octopus Cave, Dumbo Egg", self.player).item_rule =\ lambda item: item.classification != ItemClassification.progression - self.multiworld.get_location("King Jellyfish cave, bulb in the right path from King Jelly", + self.multiworld.get_location("King Jellyfish Cave, bulb in the right path from King Jelly", self.player).item_rule =\ lambda item: item.classification != ItemClassification.progression - self.multiworld.get_location("King Jellyfish cave, Jellyfish Costume", + self.multiworld.get_location("King Jellyfish Cave, Jellyfish Costume", self.player).item_rule =\ lambda item: item.classification != ItemClassification.progression - self.multiworld.get_location("Final boss area, bulb in the boss third form room", + self.multiworld.get_location("Final Boss area, bulb in the boss third form room", self.player).item_rule =\ lambda item: item.classification != ItemClassification.progression self.multiworld.get_location("Sun Worm path, first cliff bulb", @@ -1175,34 +1175,34 @@ def __no_progression_hard_or_hidden_location(self) -> None: self.multiworld.get_location("Sun Worm path, second cliff bulb", self.player).item_rule =\ lambda item: item.classification != ItemClassification.progression - self.multiworld.get_location("The veil top right area, bulb in the top of the water fall", + self.multiworld.get_location("The Veil top right area, bulb in the top of the waterfall", self.player).item_rule =\ lambda item: item.classification != ItemClassification.progression - self.multiworld.get_location("Bubble cave, bulb in the left cave wall", + self.multiworld.get_location("Bubble Cave, bulb in the left cave wall", self.player).item_rule =\ lambda item: item.classification != ItemClassification.progression - self.multiworld.get_location("Bubble cave, bulb in the right cave wall (behind the ice cristal)", + self.multiworld.get_location("Bubble Cave, bulb in the right cave wall (behind the ice crystal)", self.player).item_rule =\ lambda item: item.classification != ItemClassification.progression - self.multiworld.get_location("Bubble cave, Verse egg", + self.multiworld.get_location("Bubble Cave, Verse Egg", self.player).item_rule =\ lambda item: item.classification != ItemClassification.progression self.multiworld.get_location("Kelp Forest bottom left area, bulb close to the spirit crystals", self.player).item_rule =\ lambda item: item.classification != ItemClassification.progression - self.multiworld.get_location("Kelp forest bottom left area, Walker baby", + self.multiworld.get_location("Kelp Forest bottom left area, Walker baby", self.player).item_rule =\ lambda item: item.classification != ItemClassification.progression - self.multiworld.get_location("Sun temple, Sun key", + self.multiworld.get_location("Sun Temple, Sun Key", self.player).item_rule =\ lambda item: item.classification != ItemClassification.progression - self.multiworld.get_location("The body bottom area, Mutant Costume", + self.multiworld.get_location("The Body bottom area, Mutant Costume", self.player).item_rule =\ lambda item: item.classification != ItemClassification.progression - self.multiworld.get_location("Sun temple, bulb in the hidden room of the right part", + self.multiworld.get_location("Sun Temple, bulb in the hidden room of the right part", self.player).item_rule =\ lambda item: item.classification != ItemClassification.progression - self.multiworld.get_location("Arnassi ruins, Arnassi Armor", + self.multiworld.get_location("Arnassi Ruins, Arnassi Armor", self.player).item_rule =\ lambda item: item.classification != ItemClassification.progression @@ -1220,19 +1220,19 @@ def adjusting_rules(self, options: AquariaOptions) -> None: self.__adjusting_under_rock_location() if options.mini_bosses_to_beat.value > 0: - add_rule(self.multiworld.get_entrance("Before Final boss to Final boss", self.player), + add_rule(self.multiworld.get_entrance("Before Final Boss to Final Boss", self.player), lambda state: _has_mini_bosses(state, self.player)) if options.big_bosses_to_beat.value > 0: - add_rule(self.multiworld.get_entrance("Before Final boss to Final boss", self.player), + add_rule(self.multiworld.get_entrance("Before Final Boss to Final Boss", self.player), lambda state: _has_big_bosses(state, self.player)) if options.objective.value == 1: - add_rule(self.multiworld.get_entrance("Before Final boss to Final boss", self.player), + add_rule(self.multiworld.get_entrance("Before Final Boss to Final Boss", self.player), lambda state: _has_secrets(state, self.player)) if options.unconfine_home_water.value in [0, 1]: - add_rule(self.multiworld.get_entrance("Home Water to Home water transturtle room", self.player), + add_rule(self.multiworld.get_entrance("Home Water to Home Water transturtle room", self.player), lambda state: _has_bind_song(state, self.player)) if options.unconfine_home_water.value in [0, 2]: - add_rule(self.multiworld.get_entrance("Home Water to Open water top left area", self.player), + add_rule(self.multiworld.get_entrance("Home Water to Open Water top left area", self.player), lambda state: _has_bind_song(state, self.player) and _has_energy_form(state, self.player)) if options.early_energy_form: self.multiworld.early_items[self.player]["Energy form"] = 1 diff --git a/worlds/aquaria/__init__.py b/worlds/aquaria/__init__.py index 7c92d33a9c74..3c0cc3bdedca 100644 --- a/worlds/aquaria/__init__.py +++ b/worlds/aquaria/__init__.py @@ -71,9 +71,9 @@ class AquariaWorld(World): item_name_groups = { "Damage": {"Energy form", "Nature form", "Beast form", - "Li and Li song", "Baby nautilus", "Baby piranha", - "Baby blaster"}, - "Light": {"Sun form", "Baby dumbo"} + "Li and Li song", "Baby Nautilus", "Baby Piranha", + "Baby Blaster"}, + "Light": {"Sun form", "Baby Dumbo"} } """Grouping item make it easier to find them""" @@ -152,20 +152,20 @@ def create_items(self) -> None: precollected = [item.name for item in self.multiworld.precollected_items[self.player]] if self.options.turtle_randomizer.value > 0: if self.options.turtle_randomizer.value == 2: - self.__pre_fill_item("Transturtle Final Boss", "Final boss area, Transturtle", precollected) + self.__pre_fill_item("Transturtle Final Boss", "Final Boss area, Transturtle", precollected) else: - self.__pre_fill_item("Transturtle Veil top left", "The veil top left area, Transturtle", precollected) - self.__pre_fill_item("Transturtle Veil top right", "The veil top right area, Transturtle", precollected) - self.__pre_fill_item("Transturtle Open Water top right", "Open water top right area, Transturtle", + self.__pre_fill_item("Transturtle Veil top left", "The Veil top left area, Transturtle", precollected) + self.__pre_fill_item("Transturtle Veil top right", "The Veil top right area, Transturtle", precollected) + self.__pre_fill_item("Transturtle Open Water top right", "Open Water top right area, Transturtle", precollected) self.__pre_fill_item("Transturtle Forest bottom left", "Kelp Forest bottom left area, Transturtle", precollected) - self.__pre_fill_item("Transturtle Home water", "Home water, Transturtle", precollected) + self.__pre_fill_item("Transturtle Home Water", "Home Water, Transturtle", precollected) self.__pre_fill_item("Transturtle Abyss right", "Abyss right area, Transturtle", precollected) - self.__pre_fill_item("Transturtle Final Boss", "Final boss area, Transturtle", precollected) + self.__pre_fill_item("Transturtle Final Boss", "Final Boss area, Transturtle", precollected) # The last two are inverted because in the original game, they are special turtle that communicate directly - self.__pre_fill_item("Transturtle Simon says", "Arnassi Ruins, Transturtle", precollected) - self.__pre_fill_item("Transturtle Arnassi ruins", "Simon says area, Transturtle", precollected) + self.__pre_fill_item("Transturtle Simon Says", "Arnassi Ruins, Transturtle", precollected) + self.__pre_fill_item("Transturtle Arnassi Ruins", "Simon Says area, Transturtle", precollected) for name, data in item_table.items(): if name in precollected: precollected.remove(name) diff --git a/worlds/aquaria/docs/en_Aquaria.md b/worlds/aquaria/docs/en_Aquaria.md index c37f27568d97..c3e5f54dd66a 100644 --- a/worlds/aquaria/docs/en_Aquaria.md +++ b/worlds/aquaria/docs/en_Aquaria.md @@ -15,14 +15,14 @@ The locations in the randomizer are: - All Mithalas Urns - All Sunken City crates - Collectible treasure locations (including pet eggs and costumes) -- Beating Simon says +- Beating Simon Says - Li cave - Every Transportation Turtle (also called transturtle) - Locations where you get songs: - * Erulian spirit cristal + * Erulian spirit crystal * Energy status mini-boss * Beating Mithalan God boss - * Fish cave puzzle + * Fish Cave puzzle * Beating Drunian God boss * Beating Sun God boss * Breaking Li cage in the body @@ -61,4 +61,4 @@ what has been collected and who will receive it. ## When the player receives an item, what happens? When you receive an item, a message will pop up to inform you where you received -the item from and which one it was. \ No newline at end of file +the item from and which one it was. diff --git a/worlds/aquaria/test/__init__.py b/worlds/aquaria/test/__init__.py index ba42ac6d2c51..198ccb0f628b 100644 --- a/worlds/aquaria/test/__init__.py +++ b/worlds/aquaria/test/__init__.py @@ -10,148 +10,148 @@ # Every location accessible after the home water. after_home_water_locations = [ "Sun Crystal", - "Home water, Transturtle", - "Open water top left area, bulb under the rock in the right path", - "Open water top left area, bulb under the rock in the left path", - "Open water top left area, bulb to the right of the save cristal", - "Open water top right area, bulb in the small path before Mithalas", - "Open water top right area, bulb in the path from the left entrance", - "Open water top right area, bulb in the clearing close to the bottom exit", - "Open water top right area, bulb in the big clearing close to the save cristal", - "Open water top right area, bulb in the big clearing to the top exit", - "Open water top right area, first urn in the Mithalas exit", - "Open water top right area, second urn in the Mithalas exit", - "Open water top right area, third urn in the Mithalas exit", - "Open water top right area, bulb in the turtle room", - "Open water top right area, Transturtle", - "Open water bottom left area, bulb behind the chomper fish", - "Open water bottom left area, bulb inside the lowest fish pass", - "Open water skeleton path, bulb close to the right exit", - "Open water skeleton path, bulb behind the chomper fish", - "Open water skeleton path, King skull", + "Home Water, Transturtle", + "Open Water top left area, bulb under the rock in the right path", + "Open Water top left area, bulb under the rock in the left path", + "Open Water top left area, bulb to the right of the save crystal", + "Open Water top right area, bulb in the small path before Mithalas", + "Open Water top right area, bulb in the path from the left entrance", + "Open Water top right area, bulb in the clearing close to the bottom exit", + "Open Water top right area, bulb in the big clearing close to the save crystal", + "Open Water top right area, bulb in the big clearing to the top exit", + "Open Water top right area, first urn in the Mithalas exit", + "Open Water top right area, second urn in the Mithalas exit", + "Open Water top right area, third urn in the Mithalas exit", + "Open Water top right area, bulb in the turtle room", + "Open Water top right area, Transturtle", + "Open Water bottom left area, bulb behind the chomper fish", + "Open Water bottom left area, bulb inside the lowest fish pass", + "Open Water skeleton path, bulb close to the right exit", + "Open Water skeleton path, bulb behind the chomper fish", + "Open Water skeleton path, King Skull", "Arnassi Ruins, bulb in the right part", "Arnassi Ruins, bulb in the left part", "Arnassi Ruins, bulb in the center part", - "Arnassi ruins, Song plant spore on the top of the ruins", - "Arnassi ruins, Arnassi Armor", - "Arnassi Ruins, Arnassi statue", + "Arnassi Ruins, Song Plant Spore", + "Arnassi Ruins, Arnassi Armor", + "Arnassi Ruins, Arnassi Statue", "Arnassi Ruins, Transturtle", - "Arnassi ruins, Crab armor", - "Simon says area, Transturtle", - "Mithalas city, first bulb in the left city part", - "Mithalas city, second bulb in the left city part", - "Mithalas city, bulb in the right part", - "Mithalas city, bulb at the top of the city", - "Mithalas city, first bulb in a broken home", - "Mithalas city, second bulb in a broken home", - "Mithalas city, bulb in the bottom left part", - "Mithalas city, first bulb in one of the homes", - "Mithalas city, second bulb in one of the homes", - "Mithalas city, first urn in one of the homes", - "Mithalas city, second urn in one of the homes", - "Mithalas city, first urn in the city reserve", - "Mithalas city, second urn in the city reserve", - "Mithalas city, third urn in the city reserve", - "Mithalas city, first bulb at the end of the top path", - "Mithalas city, second bulb at the end of the top path", - "Mithalas city, bulb in the top path", - "Mithalas city, Mithalas pot", - "Mithalas city, urn in the cathedral flower tube entrance", - "Mithalas city, Doll", - "Mithalas city, urn inside a home fish pass", - "Mithalas city castle, bulb in the flesh hole", - "Mithalas city castle, Blue banner", - "Mithalas city castle, urn in the bedroom", - "Mithalas city castle, first urn of the single lamp path", - "Mithalas city castle, second urn of the single lamp path", - "Mithalas city castle, urn in the bottom room", - "Mithalas city castle, first urn on the entrance path", - "Mithalas city castle, second urn on the entrance path", - "Mithalas castle, beating the priests", - "Mithalas city castle, Trident head", - "Mithalas cathedral, first urn in the top right room", - "Mithalas cathedral, second urn in the top right room", - "Mithalas cathedral, third urn in the top right room", - "Mithalas cathedral, urn in the flesh room with fleas", - "Mithalas cathedral, first urn in the bottom right path", - "Mithalas cathedral, second urn in the bottom right path", - "Mithalas cathedral, urn behind the flesh vein", - "Mithalas cathedral, urn in the top left eyes boss room", - "Mithalas cathedral, first urn in the path behind the flesh vein", - "Mithalas cathedral, second urn in the path behind the flesh vein", - "Mithalas cathedral, third urn in the path behind the flesh vein", - "Mithalas cathedral, one of the urns in the top right room", - "Mithalas cathedral, Mithalan Dress", - "Mithalas cathedral right area, urn below the left entrance", - "Cathedral underground, bulb in the center part", - "Cathedral underground, first bulb in the top left part", - "Cathedral underground, second bulb in the top left part", - "Cathedral underground, third bulb in the top left part", - "Cathedral underground, bulb close to the save cristal", - "Cathedral underground, bulb in the bottom right path", + "Arnassi Ruins, Crab Armor", + "Simon Says area, Transturtle", + "Mithalas City, first bulb in the left city part", + "Mithalas City, second bulb in the left city part", + "Mithalas City, bulb in the right part", + "Mithalas City, bulb at the top of the city", + "Mithalas City, first bulb in a broken home", + "Mithalas City, second bulb in a broken home", + "Mithalas City, bulb in the bottom left part", + "Mithalas City, first bulb in one of the homes", + "Mithalas City, second bulb in one of the homes", + "Mithalas City, first urn in one of the homes", + "Mithalas City, second urn in one of the homes", + "Mithalas City, first urn in the city reserve", + "Mithalas City, second urn in the city reserve", + "Mithalas City, third urn in the city reserve", + "Mithalas City, first bulb at the end of the top path", + "Mithalas City, second bulb at the end of the top path", + "Mithalas City, bulb in the top path", + "Mithalas City, Mithalas Pot", + "Mithalas City, urn in the Cathedral flower tube entrance", + "Mithalas City, Doll", + "Mithalas City, urn inside a home fish pass", + "Mithalas City Castle, bulb in the flesh hole", + "Mithalas City Castle, Blue banner", + "Mithalas City Castle, urn in the bedroom", + "Mithalas City Castle, first urn of the single lamp path", + "Mithalas City Castle, second urn of the single lamp path", + "Mithalas City Castle, urn in the bottom room", + "Mithalas City Castle, first urn on the entrance path", + "Mithalas City Castle, second urn on the entrance path", + "Mithalas City Castle, beating the Priests", + "Mithalas City Castle, Trident Head", + "Mithalas Cathedral, first urn in the top right room", + "Mithalas Cathedral, second urn in the top right room", + "Mithalas Cathedral, third urn in the top right room", + "Mithalas Cathedral, urn in the flesh room with fleas", + "Mithalas Cathedral, first urn in the bottom right path", + "Mithalas Cathedral, second urn in the bottom right path", + "Mithalas Cathedral, urn behind the flesh vein", + "Mithalas Cathedral, urn in the top left eyes boss room", + "Mithalas Cathedral, first urn in the path behind the flesh vein", + "Mithalas Cathedral, second urn in the path behind the flesh vein", + "Mithalas Cathedral, third urn in the path behind the flesh vein", + "Mithalas Cathedral, fourth urn in the top right room", + "Mithalas Cathedral, Mithalan Dress", + "Mithalas Cathedral right area, urn below the left entrance", + "Cathedral Underground, bulb in the center part", + "Cathedral Underground, first bulb in the top left part", + "Cathedral Underground, second bulb in the top left part", + "Cathedral Underground, third bulb in the top left part", + "Cathedral Underground, bulb close to the save crystal", + "Cathedral Underground, bulb in the bottom right path", "Cathedral boss area, beating Mithalan God", "Kelp Forest top left area, bulb in the bottom left clearing", "Kelp Forest top left area, bulb in the path down from the top left clearing", "Kelp Forest top left area, bulb in the top left clearing", "Kelp Forest top left, Jelly Egg", - "Kelp Forest top left area, bulb close to the Verse egg", - "Kelp forest top left area, Verse egg", + "Kelp Forest top left area, bulb close to the Verse Egg", + "Kelp Forest top left area, Verse Egg", "Kelp Forest top right area, bulb under the rock in the right path", "Kelp Forest top right area, bulb at the left of the center clearing", "Kelp Forest top right area, bulb in the left path's big room", "Kelp Forest top right area, bulb in the left path's small room", "Kelp Forest top right area, bulb at the top of the center clearing", - "Kelp forest top right area, Black pearl", + "Kelp Forest top right area, Black Pearl", "Kelp Forest top right area, bulb in the top fish pass", "Kelp Forest bottom left area, bulb close to the spirit crystals", - "Kelp forest bottom left area, Walker baby", + "Kelp Forest bottom left area, Walker baby", "Kelp Forest bottom left area, Transturtle", - "Kelp forest bottom right area, Odd Container", - "Kelp forest boss area, beating Drunian God", + "Kelp Forest bottom right area, Odd Container", + "Kelp Forest boss area, beating Drunian God", "Kelp Forest boss room, bulb at the bottom of the area", - "Kelp Forest bottom left area, Fish cave puzzle", + "Kelp Forest bottom left area, Fish Cave puzzle", "Kelp Forest sprite cave, bulb inside the fish pass", "Kelp Forest sprite cave, bulb in the second room", - "Kelp Forest Sprite Cave, Seed bag", + "Kelp Forest sprite cave, Seed Bag", "Mermog cave, bulb in the left part of the cave", "Mermog cave, Piranha Egg", - "The veil top left area, In the Li cave", - "The veil top left area, bulb under the rock in the top right path", - "The veil top left area, bulb hidden behind the blocking rock", - "The veil top left area, Transturtle", - "The veil top left area, bulb inside the fish pass", + "The Veil top left area, In Li's cave", + "The Veil top left area, bulb under the rock in the top right path", + "The Veil top left area, bulb hidden behind the blocking rock", + "The Veil top left area, Transturtle", + "The Veil top left area, bulb inside the fish pass", "Turtle cave, Turtle Egg", - "Turtle cave, bulb in bubble cliff", - "Turtle cave, Urchin costume", - "The veil top right area, bulb in the middle of the wall jump cliff", - "The veil top right area, golden starfish at the bottom right of the bottom path", - "The veil top right area, bulb in the top of the water fall", - "The veil top right area, Transturtle", - "The veil bottom area, bulb in the left path", - "The veil bottom area, bulb in the spirit path", - "The veil bottom area, Verse egg", - "The veil bottom area, Stone Head", - "Octopus cave, Dumbo Egg", - "Octopus cave, bulb in the path below the octopus cave path", - "Bubble cave, bulb in the left cave wall", - "Bubble cave, bulb in the right cave wall (behind the ice cristal)", - "Bubble cave, Verse egg", - "Sun temple, bulb in the top left part", - "Sun temple, bulb in the top right part", - "Sun temple, bulb at the top of the high dark room", - "Sun temple, Golden Gear", - "Sun temple, first bulb of the temple", - "Sun temple, bulb on the left part", - "Sun temple, bulb in the hidden room of the right part", - "Sun temple, Sun key", + "Turtle cave, bulb in Bubble Cliff", + "Turtle cave, Urchin Costume", + "The Veil top right area, bulb in the middle of the wall jump cliff", + "The Veil top right area, Golden Starfish", + "The Veil top right area, bulb in the top of the waterfall", + "The Veil top right area, Transturtle", + "The Veil bottom area, bulb in the left path", + "The Veil bottom area, bulb in the spirit path", + "The Veil bottom area, Verse Egg", + "The Veil bottom area, Stone Head", + "Octopus Cave, Dumbo Egg", + "Octopus Cave, bulb in the path below the Octopus Cave path", + "Bubble Cave, bulb in the left cave wall", + "Bubble Cave, bulb in the right cave wall (behind the ice crystal)", + "Bubble Cave, Verse Egg", + "Sun Temple, bulb in the top left part", + "Sun Temple, bulb in the top right part", + "Sun Temple, bulb at the top of the high dark room", + "Sun Temple, Golden Gear", + "Sun Temple, first bulb of the temple", + "Sun Temple, bulb on the left part", + "Sun Temple, bulb in the hidden room of the right part", + "Sun Temple, Sun Key", "Sun Worm path, first path bulb", "Sun Worm path, second path bulb", "Sun Worm path, first cliff bulb", "Sun Worm path, second cliff bulb", - "Sun temple boss area, beating Sun God", + "Sun Temple boss area, beating Sun God", "Abyss left area, bulb in hidden path room", "Abyss left area, bulb in the right part", - "Abyss left area, Glowing seed", + "Abyss left area, Glowing Seed", "Abyss left area, Glowing Plant", "Abyss left area, bulb in the bottom fish pass", "Abyss right area, bulb behind the rock in the whale room", @@ -159,40 +159,40 @@ "Abyss right area, bulb behind the rock in the middle path", "Abyss right area, bulb in the left green room", "Abyss right area, Transturtle", - "Ice cave, bulb in the room to the right", - "Ice cave, First bulbs in the top exit room", - "Ice cave, Second bulbs in the top exit room", - "Ice cave, third bulbs in the top exit room", - "Ice cave, bulb in the left room", - "King Jellyfish cave, bulb in the right path from King Jelly", - "King Jellyfish cave, Jellyfish Costume", - "The whale, Verse egg", - "Sunken city right area, crate close to the save cristal", - "Sunken city right area, crate in the left bottom room", - "Sunken city left area, crate in the little pipe room", - "Sunken city left area, crate close to the save cristal", - "Sunken city left area, crate before the bedroom", - "Sunken city left area, Girl Costume", - "Sunken city, bulb on the top of the boss area (boiler room)", - "The body center area, breaking li cage", - "The body main area, bulb on the main path blocking tube", - "The body left area, first bulb in the top face room", - "The body left area, second bulb in the top face room", - "The body left area, bulb below the water stream", - "The body left area, bulb in the top path to the top face room", - "The body left area, bulb in the bottom face room", - "The body right area, bulb in the top face room", - "The body right area, bulb in the top path to the bottom face room", - "The body right area, bulb in the bottom face room", - "The body bottom area, bulb in the Jelly Zap room", - "The body bottom area, bulb in the nautilus room", - "The body bottom area, Mutant Costume", - "Final boss area, first bulb in the turtle room", - "Final boss area, second bulbs in the turtle room", - "Final boss area, third bulbs in the turtle room", - "Final boss area, Transturtle", - "Final boss area, bulb in the boss third form room", - "Kelp forest, beating Simon says", + "Ice Cave, bulb in the room to the right", + "Ice Cave, first bulb in the top exit room", + "Ice Cave, second bulb in the top exit room", + "Ice Cave, third bulb in the top exit room", + "Ice Cave, bulb in the left room", + "King Jellyfish Cave, bulb in the right path from King Jelly", + "King Jellyfish Cave, Jellyfish Costume", + "The Whale, Verse Egg", + "Sunken City right area, crate close to the save crystal", + "Sunken City right area, crate in the left bottom room", + "Sunken City left area, crate in the little pipe room", + "Sunken City left area, crate close to the save crystal", + "Sunken City left area, crate before the bedroom", + "Sunken City left area, Girl Costume", + "Sunken City, bulb on top of the boss area", + "The Body center area, breaking Li's cage", + "The Body main area, bulb on the main path blocking tube", + "The Body left area, first bulb in the top face room", + "The Body left area, second bulb in the top face room", + "The Body left area, bulb below the water stream", + "The Body left area, bulb in the top path to the top face room", + "The Body left area, bulb in the bottom face room", + "The Body right area, bulb in the top face room", + "The Body right area, bulb in the top path to the bottom face room", + "The Body right area, bulb in the bottom face room", + "The Body bottom area, bulb in the Jelly Zap room", + "The Body bottom area, bulb in the nautilus room", + "The Body bottom area, Mutant Costume", + "Final Boss area, first bulb in the turtle room", + "Final Boss area, second bulb in the turtle room", + "Final Boss area, third bulb in the turtle room", + "Final Boss area, Transturtle", + "Final Boss area, bulb in the boss third form room", + "Simon Says area, beating Simon Says", "Beating Fallen God", "Beating Mithalan God", "Beating Drunian God", diff --git a/worlds/aquaria/test/test_beast_form_access.py b/worlds/aquaria/test/test_beast_form_access.py index a8d5551586a0..c25070d470b5 100644 --- a/worlds/aquaria/test/test_beast_form_access.py +++ b/worlds/aquaria/test/test_beast_form_access.py @@ -13,33 +13,33 @@ class BeastFormAccessTest(AquariaTestBase): def test_beast_form_location(self) -> None: """Test locations that require beast form""" locations = [ - "Mithalas castle, beating the priests", - "Arnassi ruins, Crab armor", - "Arnassi ruins, Song plant spore on the top of the ruins", - "Mithalas city, first bulb at the end of the top path", - "Mithalas city, second bulb at the end of the top path", - "Mithalas city, bulb in the top path", - "Mithalas city, Mithalas pot", - "Mithalas city, urn in the cathedral flower tube entrance", + "Mithalas City Castle, beating the Priests", + "Arnassi Ruins, Crab Armor", + "Arnassi Ruins, Song Plant Spore", + "Mithalas City, first bulb at the end of the top path", + "Mithalas City, second bulb at the end of the top path", + "Mithalas City, bulb in the top path", + "Mithalas City, Mithalas Pot", + "Mithalas City, urn in the Cathedral flower tube entrance", "Mermog cave, Piranha Egg", - "Mithalas cathedral, Mithalan Dress", - "Turtle cave, bulb in bubble cliff", - "Turtle cave, Urchin costume", + "Mithalas Cathedral, Mithalan Dress", + "Turtle cave, bulb in Bubble Cliff", + "Turtle cave, Urchin Costume", "Sun Worm path, first cliff bulb", "Sun Worm path, second cliff bulb", - "The veil top right area, bulb in the top of the water fall", - "Bubble cave, bulb in the left cave wall", - "Bubble cave, bulb in the right cave wall (behind the ice cristal)", - "Bubble cave, Verse egg", - "Sunken city, bulb on the top of the boss area (boiler room)", - "Octopus cave, Dumbo Egg", + "The Veil top right area, bulb in the top of the waterfall", + "Bubble Cave, bulb in the left cave wall", + "Bubble Cave, bulb in the right cave wall (behind the ice crystal)", + "Bubble Cave, Verse Egg", + "Sunken City, bulb on top of the boss area", + "Octopus Cave, Dumbo Egg", "Beating the Golem", "Beating Mergog", "Beating Crabbius Maximus", "Beating Octopus Prime", "Beating Mantis Shrimp Prime", - "King Jellyfish cave, Jellyfish Costume", - "King Jellyfish cave, bulb in the right path from King Jelly", + "King Jellyfish Cave, Jellyfish Costume", + "King Jellyfish Cave, bulb in the right path from King Jelly", "Beating King Jellyfish God Prime", "Beating Mithalan priests", "Sunken City cleared" diff --git a/worlds/aquaria/test/test_bind_song_access.py b/worlds/aquaria/test/test_bind_song_access.py index b137d48ca9c4..ca663369cc63 100644 --- a/worlds/aquaria/test/test_bind_song_access.py +++ b/worlds/aquaria/test/test_bind_song_access.py @@ -17,19 +17,19 @@ class BindSongAccessTest(AquariaTestBase): def test_bind_song_location(self) -> None: """Test locations that require Bind song""" locations = [ - "Verse cave right area, Big Seed", - "Home water, bulb in the path below Nautilus Prime", - "Home water, bulb in the bottom left room", - "Home water, Nautilus Egg", - "Song cave, Verse egg", - "Energy temple first area, beating the energy statue", - "Energy temple first area, bulb in the bottom room blocked by a rock", - "Energy temple first area, Energy Idol", - "Energy temple second area, bulb under the rock", - "Energy temple bottom entrance, Krotite armor", - "Energy temple third area, bulb in the bottom path", - "Energy temple boss area, Fallen god tooth", - "Energy temple blaster room, Blaster egg", + "Verse Cave right area, Big Seed", + "Home Water, bulb in the path below Nautilus Prime", + "Home Water, bulb in the bottom left room", + "Home Water, Nautilus Egg", + "Song Cave, Verse Egg", + "Energy Temple first area, beating the Energy Statue", + "Energy Temple first area, bulb in the bottom room blocked by a rock", + "Energy Temple first area, Energy Idol", + "Energy Temple second area, bulb under the rock", + "Energy Temple bottom entrance, Krotite Armor", + "Energy Temple third area, bulb in the bottom path", + "Energy Temple boss area, Fallen God Tooth", + "Energy Temple blaster room, Blaster Egg", *after_home_water_locations ] items = [["Bind song"]] diff --git a/worlds/aquaria/test/test_bind_song_option_access.py b/worlds/aquaria/test/test_bind_song_option_access.py index 522a064b6251..a75ef60cdf05 100644 --- a/worlds/aquaria/test/test_bind_song_option_access.py +++ b/worlds/aquaria/test/test_bind_song_option_access.py @@ -18,24 +18,24 @@ class BindSongOptionAccessTest(AquariaTestBase): def test_bind_song_location(self) -> None: """Test locations that require Bind song with the bind song needed option activated""" locations = [ - "Verse cave right area, Big Seed", - "Verse cave left area, bulb under the rock at the end of the path", - "Home water, bulb under the rock in the left path from the verse cave", - "Song cave, bulb under the rock close to the song door", - "Song cave, bulb under the rock in the path to the singing statues", - "Naija's home, bulb under the rock at the right of the main path", - "Home water, bulb in the path below Nautilus Prime", - "Home water, bulb in the bottom left room", - "Home water, Nautilus Egg", - "Song cave, Verse egg", - "Energy temple first area, beating the energy statue", - "Energy temple first area, bulb in the bottom room blocked by a rock", - "Energy temple first area, Energy Idol", - "Energy temple second area, bulb under the rock", - "Energy temple bottom entrance, Krotite armor", - "Energy temple third area, bulb in the bottom path", - "Energy temple boss area, Fallen god tooth", - "Energy temple blaster room, Blaster egg", + "Verse Cave right area, Big Seed", + "Verse Cave left area, bulb under the rock at the end of the path", + "Home Water, bulb under the rock in the left path from the Verse Cave", + "Song Cave, bulb under the rock close to the song door", + "Song Cave, bulb under the rock in the path to the singing statues", + "Naija's Home, bulb under the rock at the right of the main path", + "Home Water, bulb in the path below Nautilus Prime", + "Home Water, bulb in the bottom left room", + "Home Water, Nautilus Egg", + "Song Cave, Verse Egg", + "Energy Temple first area, beating the Energy Statue", + "Energy Temple first area, bulb in the bottom room blocked by a rock", + "Energy Temple first area, Energy Idol", + "Energy Temple second area, bulb under the rock", + "Energy Temple bottom entrance, Krotite Armor", + "Energy Temple third area, bulb in the bottom path", + "Energy Temple boss area, Fallen God Tooth", + "Energy Temple blaster room, Blaster Egg", *after_home_water_locations ] items = [["Bind song"]] diff --git a/worlds/aquaria/test/test_confined_home_water.py b/worlds/aquaria/test/test_confined_home_water.py index f4e0e7b67962..72fddfb4048a 100644 --- a/worlds/aquaria/test/test_confined_home_water.py +++ b/worlds/aquaria/test/test_confined_home_water.py @@ -16,5 +16,5 @@ class ConfinedHomeWaterAccessTest(AquariaTestBase): def test_confine_home_water_location(self) -> None: """Test region accessible with confined home water""" - self.assertFalse(self.can_reach_region("Open water top left area"), "Can reach Open water top left area") - self.assertFalse(self.can_reach_region("Home Water, turtle room"), "Can reach Home Water, turtle room") \ No newline at end of file + self.assertFalse(self.can_reach_region("Open Water top left area"), "Can reach Open Water top left area") + self.assertFalse(self.can_reach_region("Home Water, turtle room"), "Can reach Home Water, turtle room") diff --git a/worlds/aquaria/test/test_dual_song_access.py b/worlds/aquaria/test/test_dual_song_access.py index 14c921d7cfeb..8266ffb181d9 100644 --- a/worlds/aquaria/test/test_dual_song_access.py +++ b/worlds/aquaria/test/test_dual_song_access.py @@ -16,10 +16,10 @@ class LiAccessTest(AquariaTestBase): def test_li_song_location(self) -> None: """Test locations that require the dual song""" locations = [ - "The body bottom area, bulb in the Jelly Zap room", - "The body bottom area, bulb in the nautilus room", - "The body bottom area, Mutant Costume", - "Final boss area, bulb in the boss third form room", + "The Body bottom area, bulb in the Jelly Zap room", + "The Body bottom area, bulb in the nautilus room", + "The Body bottom area, Mutant Costume", + "Final Boss area, bulb in the boss third form room", "Objective complete" ] items = [["Dual form"]] diff --git a/worlds/aquaria/test/test_energy_form_access.py b/worlds/aquaria/test/test_energy_form_access.py index edfe8a3f6c19..ce4ed4099416 100644 --- a/worlds/aquaria/test/test_energy_form_access.py +++ b/worlds/aquaria/test/test_energy_form_access.py @@ -17,41 +17,41 @@ class EnergyFormAccessTest(AquariaTestBase): def test_energy_form_location(self) -> None: """Test locations that require Energy form""" locations = [ - "Home water, Nautilus Egg", - "Naija's home, bulb after the energy door", - "Energy temple first area, bulb in the bottom room blocked by a rock", - "Energy temple second area, bulb under the rock", - "Energy temple bottom entrance, Krotite armor", - "Energy temple third area, bulb in the bottom path", - "Energy temple boss area, Fallen god tooth", - "Energy temple blaster room, Blaster egg", - "Mithalas castle, beating the priests", - "Mithalas cathedral, first urn in the top right room", - "Mithalas cathedral, second urn in the top right room", - "Mithalas cathedral, third urn in the top right room", - "Mithalas cathedral, urn in the flesh room with fleas", - "Mithalas cathedral, first urn in the bottom right path", - "Mithalas cathedral, second urn in the bottom right path", - "Mithalas cathedral, urn behind the flesh vein", - "Mithalas cathedral, urn in the top left eyes boss room", - "Mithalas cathedral, first urn in the path behind the flesh vein", - "Mithalas cathedral, second urn in the path behind the flesh vein", - "Mithalas cathedral, third urn in the path behind the flesh vein", - "Mithalas cathedral, one of the urns in the top right room", - "Mithalas cathedral, Mithalan Dress", - "Mithalas cathedral right area, urn below the left entrance", + "Home Water, Nautilus Egg", + "Naija's Home, bulb after the energy door", + "Energy Temple first area, bulb in the bottom room blocked by a rock", + "Energy Temple second area, bulb under the rock", + "Energy Temple bottom entrance, Krotite Armor", + "Energy Temple third area, bulb in the bottom path", + "Energy Temple boss area, Fallen God Tooth", + "Energy Temple blaster room, Blaster Egg", + "Mithalas City Castle, beating the Priests", + "Mithalas Cathedral, first urn in the top right room", + "Mithalas Cathedral, second urn in the top right room", + "Mithalas Cathedral, third urn in the top right room", + "Mithalas Cathedral, urn in the flesh room with fleas", + "Mithalas Cathedral, first urn in the bottom right path", + "Mithalas Cathedral, second urn in the bottom right path", + "Mithalas Cathedral, urn behind the flesh vein", + "Mithalas Cathedral, urn in the top left eyes boss room", + "Mithalas Cathedral, first urn in the path behind the flesh vein", + "Mithalas Cathedral, second urn in the path behind the flesh vein", + "Mithalas Cathedral, third urn in the path behind the flesh vein", + "Mithalas Cathedral, fourth urn in the top right room", + "Mithalas Cathedral, Mithalan Dress", + "Mithalas Cathedral right area, urn below the left entrance", "Cathedral boss area, beating Mithalan God", - "Kelp Forest top left area, bulb close to the Verse egg", - "Kelp forest top left area, Verse egg", - "Kelp forest boss area, beating Drunian God", + "Kelp Forest top left area, bulb close to the Verse Egg", + "Kelp Forest top left area, Verse Egg", + "Kelp Forest boss area, beating Drunian God", "Mermog cave, Piranha Egg", - "Octopus cave, Dumbo Egg", - "Sun temple boss area, beating Sun God", - "Arnassi ruins, Crab armor", - "King Jellyfish cave, bulb in the right path from King Jelly", - "King Jellyfish cave, Jellyfish Costume", - "Sunken city, bulb on the top of the boss area (boiler room)", - "Final boss area, bulb in the boss third form room", + "Octopus Cave, Dumbo Egg", + "Sun Temple boss area, beating Sun God", + "Arnassi Ruins, Crab Armor", + "King Jellyfish Cave, bulb in the right path from King Jelly", + "King Jellyfish Cave, Jellyfish Costume", + "Sunken City, bulb on top of the boss area", + "Final Boss area, bulb in the boss third form room", "Beating Fallen God", "Beating Mithalan God", "Beating Drunian God", @@ -69,4 +69,4 @@ def test_energy_form_location(self) -> None: "Objective complete", ] items = [["Energy form"]] - self.assertAccessDependency(locations, items) \ No newline at end of file + self.assertAccessDependency(locations, items) diff --git a/worlds/aquaria/test/test_fish_form_access.py b/worlds/aquaria/test/test_fish_form_access.py index 30772371721f..d252bb1f1862 100644 --- a/worlds/aquaria/test/test_fish_form_access.py +++ b/worlds/aquaria/test/test_fish_form_access.py @@ -16,22 +16,22 @@ class FishFormAccessTest(AquariaTestBase): def test_fish_form_location(self) -> None: """Test locations that require fish form""" locations = [ - "The veil top left area, bulb inside the fish pass", - "Mithalas city, Doll", - "Mithalas city, urn inside a home fish pass", + "The Veil top left area, bulb inside the fish pass", + "Mithalas City, Doll", + "Mithalas City, urn inside a home fish pass", "Kelp Forest top right area, bulb in the top fish pass", - "The veil bottom area, Verse egg", - "Open water bottom left area, bulb inside the lowest fish pass", - "Kelp Forest top left area, bulb close to the Verse egg", - "Kelp forest top left area, Verse egg", + "The Veil bottom area, Verse Egg", + "Open Water bottom left area, bulb inside the lowest fish pass", + "Kelp Forest top left area, bulb close to the Verse Egg", + "Kelp Forest top left area, Verse Egg", "Mermog cave, bulb in the left part of the cave", "Mermog cave, Piranha Egg", "Beating Mergog", - "Octopus cave, Dumbo Egg", - "Octopus cave, bulb in the path below the octopus cave path", + "Octopus Cave, Dumbo Egg", + "Octopus Cave, bulb in the path below the Octopus Cave path", "Beating Octopus Prime", "Abyss left area, bulb in the bottom fish pass", - "Arnassi ruins, Arnassi Armor" + "Arnassi Ruins, Arnassi Armor" ] items = [["Fish form"]] self.assertAccessDependency(locations, items) diff --git a/worlds/aquaria/test/test_li_song_access.py b/worlds/aquaria/test/test_li_song_access.py index e26d5b5fcd93..42adc90e5aa1 100644 --- a/worlds/aquaria/test/test_li_song_access.py +++ b/worlds/aquaria/test/test_li_song_access.py @@ -16,27 +16,27 @@ class LiAccessTest(AquariaTestBase): def test_li_song_location(self) -> None: """Test locations that require Li""" locations = [ - "Sunken city right area, crate close to the save cristal", - "Sunken city right area, crate in the left bottom room", - "Sunken city left area, crate in the little pipe room", - "Sunken city left area, crate close to the save cristal", - "Sunken city left area, crate before the bedroom", - "Sunken city left area, Girl Costume", - "Sunken city, bulb on the top of the boss area (boiler room)", - "The body center area, breaking li cage", - "The body main area, bulb on the main path blocking tube", - "The body left area, first bulb in the top face room", - "The body left area, second bulb in the top face room", - "The body left area, bulb below the water stream", - "The body left area, bulb in the top path to the top face room", - "The body left area, bulb in the bottom face room", - "The body right area, bulb in the top face room", - "The body right area, bulb in the top path to the bottom face room", - "The body right area, bulb in the bottom face room", - "The body bottom area, bulb in the Jelly Zap room", - "The body bottom area, bulb in the nautilus room", - "The body bottom area, Mutant Costume", - "Final boss area, bulb in the boss third form room", + "Sunken City right area, crate close to the save crystal", + "Sunken City right area, crate in the left bottom room", + "Sunken City left area, crate in the little pipe room", + "Sunken City left area, crate close to the save crystal", + "Sunken City left area, crate before the bedroom", + "Sunken City left area, Girl Costume", + "Sunken City, bulb on top of the boss area", + "The Body center area, breaking Li's cage", + "The Body main area, bulb on the main path blocking tube", + "The Body left area, first bulb in the top face room", + "The Body left area, second bulb in the top face room", + "The Body left area, bulb below the water stream", + "The Body left area, bulb in the top path to the top face room", + "The Body left area, bulb in the bottom face room", + "The Body right area, bulb in the top face room", + "The Body right area, bulb in the top path to the bottom face room", + "The Body right area, bulb in the bottom face room", + "The Body bottom area, bulb in the Jelly Zap room", + "The Body bottom area, bulb in the nautilus room", + "The Body bottom area, Mutant Costume", + "Final Boss area, bulb in the boss third form room", "Beating the Golem", "Sunken City cleared", "Objective complete" diff --git a/worlds/aquaria/test/test_light_access.py b/worlds/aquaria/test/test_light_access.py index 49414e5ace9d..41e65cb30d9b 100644 --- a/worlds/aquaria/test/test_light_access.py +++ b/worlds/aquaria/test/test_light_access.py @@ -20,19 +20,19 @@ def test_light_location(self) -> None: # Since the `assertAccessDependency` sweep for events even if I tell it not to, those location cannot be # tested. # "Third secret", - # "Sun temple, bulb in the top left part", - # "Sun temple, bulb in the top right part", - # "Sun temple, bulb at the top of the high dark room", - # "Sun temple, Golden Gear", + # "Sun Temple, bulb in the top left part", + # "Sun Temple, bulb in the top right part", + # "Sun Temple, bulb at the top of the high dark room", + # "Sun Temple, Golden Gear", # "Sun Worm path, first path bulb", # "Sun Worm path, second path bulb", # "Sun Worm path, first cliff bulb", - "Octopus cave, Dumbo Egg", - "Kelp forest bottom right area, Odd Container", - "Kelp forest top right area, Black pearl", + "Octopus Cave, Dumbo Egg", + "Kelp Forest bottom right area, Odd Container", + "Kelp Forest top right area, Black Pearl", "Abyss left area, bulb in hidden path room", "Abyss left area, bulb in the right part", - "Abyss left area, Glowing seed", + "Abyss left area, Glowing Seed", "Abyss left area, Glowing Plant", "Abyss left area, bulb in the bottom fish pass", "Abyss right area, bulb behind the rock in the whale room", @@ -40,32 +40,32 @@ def test_light_location(self) -> None: "Abyss right area, bulb behind the rock in the middle path", "Abyss right area, bulb in the left green room", "Abyss right area, Transturtle", - "Ice cave, bulb in the room to the right", - "Ice cave, First bulbs in the top exit room", - "Ice cave, Second bulbs in the top exit room", - "Ice cave, third bulbs in the top exit room", - "Ice cave, bulb in the left room", - "Bubble cave, bulb in the left cave wall", - "Bubble cave, bulb in the right cave wall (behind the ice cristal)", - "Bubble cave, Verse egg", + "Ice Cave, bulb in the room to the right", + "Ice Cave, first bulb in the top exit room", + "Ice Cave, second bulb in the top exit room", + "Ice Cave, third bulb in the top exit room", + "Ice Cave, bulb in the left room", + "Bubble Cave, bulb in the left cave wall", + "Bubble Cave, bulb in the right cave wall (behind the ice crystal)", + "Bubble Cave, Verse Egg", "Beating Mantis Shrimp Prime", - "King Jellyfish cave, bulb in the right path from King Jelly", - "King Jellyfish cave, Jellyfish Costume", + "King Jellyfish Cave, bulb in the right path from King Jelly", + "King Jellyfish Cave, Jellyfish Costume", "Beating King Jellyfish God Prime", - "The whale, Verse egg", + "The Whale, Verse Egg", "First secret", - "Sunken city right area, crate close to the save cristal", - "Sunken city right area, crate in the left bottom room", - "Sunken city left area, crate in the little pipe room", - "Sunken city left area, crate close to the save cristal", - "Sunken city left area, crate before the bedroom", - "Sunken city left area, Girl Costume", - "Sunken city, bulb on the top of the boss area (boiler room)", + "Sunken City right area, crate close to the save crystal", + "Sunken City right area, crate in the left bottom room", + "Sunken City left area, crate in the little pipe room", + "Sunken City left area, crate close to the save crystal", + "Sunken City left area, crate before the bedroom", + "Sunken City left area, Girl Costume", + "Sunken City, bulb on top of the boss area", "Sunken City cleared", "Beating the Golem", "Beating Octopus Prime", - "Final boss area, bulb in the boss third form room", + "Final Boss area, bulb in the boss third form room", "Objective complete", ] - items = [["Sun form", "Baby dumbo", "Has sun crystal"]] + items = [["Sun form", "Baby Dumbo", "Has sun crystal"]] self.assertAccessDependency(locations, items) diff --git a/worlds/aquaria/test/test_nature_form_access.py b/worlds/aquaria/test/test_nature_form_access.py index 89e7ceecbbd9..b380e5048fc9 100644 --- a/worlds/aquaria/test/test_nature_form_access.py +++ b/worlds/aquaria/test/test_nature_form_access.py @@ -16,41 +16,41 @@ class NatureFormAccessTest(AquariaTestBase): def test_nature_form_location(self) -> None: """Test locations that require nature form""" locations = [ - "Song cave, Anemone seed", - "Energy temple blaster room, Blaster egg", + "Song Cave, Anemone Seed", + "Energy Temple blaster room, Blaster Egg", "Beating Blaster Peg Prime", - "Kelp forest top left area, Verse egg", - "Kelp Forest top left area, bulb close to the Verse egg", - "Mithalas castle, beating the priests", + "Kelp Forest top left area, Verse Egg", + "Kelp Forest top left area, bulb close to the Verse Egg", + "Mithalas City Castle, beating the Priests", "Kelp Forest sprite cave, bulb in the second room", - "Kelp Forest Sprite Cave, Seed bag", + "Kelp Forest sprite cave, Seed Bag", "Beating Mithalan priests", "Abyss left area, bulb in the bottom fish pass", - "Bubble cave, Verse egg", + "Bubble Cave, Verse Egg", "Beating Mantis Shrimp Prime", - "Sunken city right area, crate close to the save cristal", - "Sunken city right area, crate in the left bottom room", - "Sunken city left area, crate in the little pipe room", - "Sunken city left area, crate close to the save cristal", - "Sunken city left area, crate before the bedroom", - "Sunken city left area, Girl Costume", - "Sunken city, bulb on the top of the boss area (boiler room)", + "Sunken City right area, crate close to the save crystal", + "Sunken City right area, crate in the left bottom room", + "Sunken City left area, crate in the little pipe room", + "Sunken City left area, crate close to the save crystal", + "Sunken City left area, crate before the bedroom", + "Sunken City left area, Girl Costume", + "Sunken City, bulb on top of the boss area", "Beating the Golem", "Sunken City cleared", - "The body center area, breaking li cage", - "The body main area, bulb on the main path blocking tube", - "The body left area, first bulb in the top face room", - "The body left area, second bulb in the top face room", - "The body left area, bulb below the water stream", - "The body left area, bulb in the top path to the top face room", - "The body left area, bulb in the bottom face room", - "The body right area, bulb in the top face room", - "The body right area, bulb in the top path to the bottom face room", - "The body right area, bulb in the bottom face room", - "The body bottom area, bulb in the Jelly Zap room", - "The body bottom area, bulb in the nautilus room", - "The body bottom area, Mutant Costume", - "Final boss area, bulb in the boss third form room", + "The Body center area, breaking Li's cage", + "The Body main area, bulb on the main path blocking tube", + "The Body left area, first bulb in the top face room", + "The Body left area, second bulb in the top face room", + "The Body left area, bulb below the water stream", + "The Body left area, bulb in the top path to the top face room", + "The Body left area, bulb in the bottom face room", + "The Body right area, bulb in the top face room", + "The Body right area, bulb in the top path to the bottom face room", + "The Body right area, bulb in the bottom face room", + "The Body bottom area, bulb in the Jelly Zap room", + "The Body bottom area, bulb in the nautilus room", + "The Body bottom area, Mutant Costume", + "Final Boss area, bulb in the boss third form room", "Objective complete" ] items = [["Nature form"]] diff --git a/worlds/aquaria/test/test_no_progression_hard_hidden_locations.py b/worlds/aquaria/test/test_no_progression_hard_hidden_locations.py index 5876ff31aa0f..817b9547a892 100644 --- a/worlds/aquaria/test/test_no_progression_hard_hidden_locations.py +++ b/worlds/aquaria/test/test_no_progression_hard_hidden_locations.py @@ -15,31 +15,31 @@ class UNoProgressionHardHiddenTest(AquariaTestBase): } unfillable_locations = [ - "Energy temple boss area, Fallen god tooth", + "Energy Temple boss area, Fallen God Tooth", "Cathedral boss area, beating Mithalan God", - "Kelp forest boss area, beating Drunian God", - "Sun temple boss area, beating Sun God", - "Sunken city, bulb on the top of the boss area (boiler room)", - "Home water, Nautilus Egg", - "Energy temple blaster room, Blaster egg", - "Mithalas castle, beating the priests", + "Kelp Forest boss area, beating Drunian God", + "Sun Temple boss area, beating Sun God", + "Sunken City, bulb on top of the boss area", + "Home Water, Nautilus Egg", + "Energy Temple blaster room, Blaster Egg", + "Mithalas City Castle, beating the Priests", "Mermog cave, Piranha Egg", - "Octopus cave, Dumbo Egg", - "King Jellyfish cave, bulb in the right path from King Jelly", - "King Jellyfish cave, Jellyfish Costume", - "Final boss area, bulb in the boss third form room", + "Octopus Cave, Dumbo Egg", + "King Jellyfish Cave, bulb in the right path from King Jelly", + "King Jellyfish Cave, Jellyfish Costume", + "Final Boss area, bulb in the boss third form room", "Sun Worm path, first cliff bulb", "Sun Worm path, second cliff bulb", - "The veil top right area, bulb in the top of the water fall", - "Bubble cave, bulb in the left cave wall", - "Bubble cave, bulb in the right cave wall (behind the ice cristal)", - "Bubble cave, Verse egg", + "The Veil top right area, bulb in the top of the waterfall", + "Bubble Cave, bulb in the left cave wall", + "Bubble Cave, bulb in the right cave wall (behind the ice crystal)", + "Bubble Cave, Verse Egg", "Kelp Forest bottom left area, bulb close to the spirit crystals", - "Kelp forest bottom left area, Walker baby", - "Sun temple, Sun key", - "The body bottom area, Mutant Costume", - "Sun temple, bulb in the hidden room of the right part", - "Arnassi ruins, Arnassi Armor", + "Kelp Forest bottom left area, Walker baby", + "Sun Temple, Sun Key", + "The Body bottom area, Mutant Costume", + "Sun Temple, bulb in the hidden room of the right part", + "Arnassi Ruins, Arnassi Armor", ] def test_unconfine_home_water_both_location_fillable(self) -> None: diff --git a/worlds/aquaria/test/test_progression_hard_hidden_locations.py b/worlds/aquaria/test/test_progression_hard_hidden_locations.py index 6450236097c9..2b7c8ddac93a 100644 --- a/worlds/aquaria/test/test_progression_hard_hidden_locations.py +++ b/worlds/aquaria/test/test_progression_hard_hidden_locations.py @@ -15,31 +15,31 @@ class UNoProgressionHardHiddenTest(AquariaTestBase): } unfillable_locations = [ - "Energy temple boss area, Fallen god tooth", + "Energy Temple boss area, Fallen God Tooth", "Cathedral boss area, beating Mithalan God", - "Kelp forest boss area, beating Drunian God", - "Sun temple boss area, beating Sun God", - "Sunken city, bulb on the top of the boss area (boiler room)", - "Home water, Nautilus Egg", - "Energy temple blaster room, Blaster egg", - "Mithalas castle, beating the priests", + "Kelp Forest boss area, beating Drunian God", + "Sun Temple boss area, beating Sun God", + "Sunken City, bulb on top of the boss area", + "Home Water, Nautilus Egg", + "Energy Temple blaster room, Blaster Egg", + "Mithalas City Castle, beating the Priests", "Mermog cave, Piranha Egg", - "Octopus cave, Dumbo Egg", - "King Jellyfish cave, bulb in the right path from King Jelly", - "King Jellyfish cave, Jellyfish Costume", - "Final boss area, bulb in the boss third form room", + "Octopus Cave, Dumbo Egg", + "King Jellyfish Cave, bulb in the right path from King Jelly", + "King Jellyfish Cave, Jellyfish Costume", + "Final Boss area, bulb in the boss third form room", "Sun Worm path, first cliff bulb", "Sun Worm path, second cliff bulb", - "The veil top right area, bulb in the top of the water fall", - "Bubble cave, bulb in the left cave wall", - "Bubble cave, bulb in the right cave wall (behind the ice cristal)", - "Bubble cave, Verse egg", + "The Veil top right area, bulb in the top of the waterfall", + "Bubble Cave, bulb in the left cave wall", + "Bubble Cave, bulb in the right cave wall (behind the ice crystal)", + "Bubble Cave, Verse Egg", "Kelp Forest bottom left area, bulb close to the spirit crystals", - "Kelp forest bottom left area, Walker baby", - "Sun temple, Sun key", - "The body bottom area, Mutant Costume", - "Sun temple, bulb in the hidden room of the right part", - "Arnassi ruins, Arnassi Armor", + "Kelp Forest bottom left area, Walker baby", + "Sun Temple, Sun Key", + "The Body bottom area, Mutant Costume", + "Sun Temple, bulb in the hidden room of the right part", + "Arnassi Ruins, Arnassi Armor", ] def test_unconfine_home_water_both_location_fillable(self) -> None: diff --git a/worlds/aquaria/test/test_spirit_form_access.py b/worlds/aquaria/test/test_spirit_form_access.py index 4d59d90a4011..a6eec0da5dd3 100644 --- a/worlds/aquaria/test/test_spirit_form_access.py +++ b/worlds/aquaria/test/test_spirit_form_access.py @@ -13,24 +13,24 @@ class SpiritFormAccessTest(AquariaTestBase): def test_spirit_form_location(self) -> None: """Test locations that require spirit form""" locations = [ - "The veil bottom area, bulb in the spirit path", - "Mithalas city castle, Trident head", - "Open water skeleton path, King skull", - "Kelp forest bottom left area, Walker baby", + "The Veil bottom area, bulb in the spirit path", + "Mithalas City Castle, Trident Head", + "Open Water skeleton path, King Skull", + "Kelp Forest bottom left area, Walker baby", "Abyss right area, bulb behind the rock in the whale room", - "The whale, Verse egg", - "Ice cave, bulb in the room to the right", - "Ice cave, First bulbs in the top exit room", - "Ice cave, Second bulbs in the top exit room", - "Ice cave, third bulbs in the top exit room", - "Ice cave, bulb in the left room", - "Bubble cave, bulb in the left cave wall", - "Bubble cave, bulb in the right cave wall (behind the ice cristal)", - "Bubble cave, Verse egg", - "Sunken city left area, Girl Costume", + "The Whale, Verse Egg", + "Ice Cave, bulb in the room to the right", + "Ice Cave, first bulb in the top exit room", + "Ice Cave, second bulb in the top exit room", + "Ice Cave, third bulb in the top exit room", + "Ice Cave, bulb in the left room", + "Bubble Cave, bulb in the left cave wall", + "Bubble Cave, bulb in the right cave wall (behind the ice crystal)", + "Bubble Cave, Verse Egg", + "Sunken City left area, Girl Costume", "Beating Mantis Shrimp Prime", "First secret", - "Arnassi ruins, Arnassi Armor", + "Arnassi Ruins, Arnassi Armor", ] items = [["Spirit form"]] self.assertAccessDependency(locations, items) diff --git a/worlds/aquaria/test/test_sun_form_access.py b/worlds/aquaria/test/test_sun_form_access.py index 159ab717c2ac..dfd732ec910c 100644 --- a/worlds/aquaria/test/test_sun_form_access.py +++ b/worlds/aquaria/test/test_sun_form_access.py @@ -14,11 +14,11 @@ def test_sun_form_location(self) -> None: """Test locations that require sun form""" locations = [ "First secret", - "The whale, Verse egg", + "The Whale, Verse Egg", "Abyss right area, bulb behind the rock in the whale room", - "Octopus cave, Dumbo Egg", + "Octopus Cave, Dumbo Egg", "Beating Octopus Prime", - "Final boss area, bulb in the boss third form room", + "Final Boss area, bulb in the boss third form room", "Objective complete" ] items = [["Sun form"]] diff --git a/worlds/aquaria/test/test_unconfine_home_water_via_both.py b/worlds/aquaria/test/test_unconfine_home_water_via_both.py index 3af17f1b75d1..24d3adad9745 100644 --- a/worlds/aquaria/test/test_unconfine_home_water_via_both.py +++ b/worlds/aquaria/test/test_unconfine_home_water_via_both.py @@ -17,5 +17,5 @@ class UnconfineHomeWaterBothAccessTest(AquariaTestBase): def test_unconfine_home_water_both_location(self) -> None: """Test locations accessible with unconfined home water via energy door and transportation turtle""" - self.assertTrue(self.can_reach_region("Open water top left area"), "Cannot reach Open water top left area") - self.assertTrue(self.can_reach_region("Home Water, turtle room"), "Cannot reach Home Water, turtle room") \ No newline at end of file + self.assertTrue(self.can_reach_region("Open Water top left area"), "Cannot reach Open Water top left area") + self.assertTrue(self.can_reach_region("Home Water, turtle room"), "Cannot reach Home Water, turtle room") diff --git a/worlds/aquaria/test/test_unconfine_home_water_via_energy_door.py b/worlds/aquaria/test/test_unconfine_home_water_via_energy_door.py index bfa82d65eac9..92eb8d029135 100644 --- a/worlds/aquaria/test/test_unconfine_home_water_via_energy_door.py +++ b/worlds/aquaria/test/test_unconfine_home_water_via_energy_door.py @@ -16,5 +16,5 @@ class UnconfineHomeWaterEnergyDoorAccessTest(AquariaTestBase): def test_unconfine_home_water_energy_door_location(self) -> None: """Test locations accessible with unconfined home water via energy door""" - self.assertTrue(self.can_reach_region("Open water top left area"), "Cannot reach Open water top left area") - self.assertFalse(self.can_reach_region("Home Water, turtle room"), "Can reach Home Water, turtle room") \ No newline at end of file + self.assertTrue(self.can_reach_region("Open Water top left area"), "Cannot reach Open Water top left area") + self.assertFalse(self.can_reach_region("Home Water, turtle room"), "Can reach Home Water, turtle room") diff --git a/worlds/aquaria/test/test_unconfine_home_water_via_transturtle.py b/worlds/aquaria/test/test_unconfine_home_water_via_transturtle.py index 627a92db2918..66c40d23f1d8 100644 --- a/worlds/aquaria/test/test_unconfine_home_water_via_transturtle.py +++ b/worlds/aquaria/test/test_unconfine_home_water_via_transturtle.py @@ -17,4 +17,4 @@ class UnconfineHomeWaterTransturtleAccessTest(AquariaTestBase): def test_unconfine_home_water_transturtle_location(self) -> None: """Test locations accessible with unconfined home water via transportation turtle""" self.assertTrue(self.can_reach_region("Home Water, turtle room"), "Cannot reach Home Water, turtle room") - self.assertFalse(self.can_reach_region("Open water top left area"), "Can reach Open water top left area") \ No newline at end of file + self.assertFalse(self.can_reach_region("Open Water top left area"), "Can reach Open Water top left area") From 91c89604a58746e632c91083d10e629be36d9bcc Mon Sep 17 00:00:00 2001 From: Rensen3 <127029481+Rensen3@users.noreply.github.com> Date: Sat, 1 Jun 2024 13:10:02 +0200 Subject: [PATCH 43/64] YGO06: prevent multiple players affecting each others procedure patch (#3409) --- worlds/yugioh06/__init__.py | 6 ++++-- worlds/yugioh06/rom.py | 2 -- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/worlds/yugioh06/__init__.py b/worlds/yugioh06/__init__.py index 2640b13aca4b..1cf44f090fed 100644 --- a/worlds/yugioh06/__init__.py +++ b/worlds/yugioh06/__init__.py @@ -399,12 +399,14 @@ def generate_output(self, output_directory: str): self.playerName.extend([0] * (0x20 - len(self.playerName))) patch = YGO06ProcedurePatch(player=self.player, player_name=self.multiworld.player_name[self.player]) patch.write_file("base_patch.bsdiff4", pkgutil.get_data(__name__, "patch.bsdiff4")) + procedure = [("apply_bsdiff4", ["base_patch.bsdiff4"]), ("apply_tokens", ["token_data.bin"])] if self.is_draft_mode: - patch.procedure.insert(1, ("apply_bsdiff4", ["draft_patch.bsdiff4"])) + procedure.insert(1, ("apply_bsdiff4", ["draft_patch.bsdiff4"])) patch.write_file("draft_patch.bsdiff4", pkgutil.get_data(__name__, "patches/draft.bsdiff4")) if self.options.ocg_arts: - patch.procedure.insert(1, ("apply_bsdiff4", ["ocg_patch.bsdiff4"])) + procedure.insert(1, ("apply_bsdiff4", ["ocg_patch.bsdiff4"])) patch.write_file("ocg_patch.bsdiff4", pkgutil.get_data(__name__, "patches/ocg.bsdiff4")) + patch.procedure = procedure write_tokens(self, patch) # Write Output diff --git a/worlds/yugioh06/rom.py b/worlds/yugioh06/rom.py index 0bd3f1cb7689..3ac10f9ea496 100644 --- a/worlds/yugioh06/rom.py +++ b/worlds/yugioh06/rom.py @@ -22,8 +22,6 @@ class YGO06ProcedurePatch(APProcedurePatch, APTokenMixin): patch_file_ending = ".apygo06" result_file_ending = ".gba" - procedure = [("apply_bsdiff4", ["base_patch.bsdiff4"]), ("apply_tokens", ["token_data.bin"])] - @classmethod def get_source_data(cls) -> bytes: return get_base_rom_bytes() From 67cd32b37c4e5fee871685c3e40bc1bf2470b6a0 Mon Sep 17 00:00:00 2001 From: Bryce Wilson Date: Sat, 1 Jun 2024 04:12:37 -0700 Subject: [PATCH 44/64] Pokemon Emerald: Use `self.player_name` (#3384) --- worlds/pokemon_emerald/__init__.py | 20 ++++++++++---------- worlds/pokemon_emerald/client.py | 2 +- worlds/pokemon_emerald/rom.py | 4 ++-- 3 files changed, 13 insertions(+), 13 deletions(-) diff --git a/worlds/pokemon_emerald/__init__.py b/worlds/pokemon_emerald/__init__.py index 3e50f748d922..aa4f6ccf7519 100644 --- a/worlds/pokemon_emerald/__init__.py +++ b/worlds/pokemon_emerald/__init__.py @@ -174,26 +174,26 @@ def generate_early(self) -> None: # In race mode we don't patch any item location information into the ROM if self.multiworld.is_race and not self.options.remote_items: logging.warning("Pokemon Emerald: Forcing Player %s (%s) to use remote items due to race mode.", - self.player, self.multiworld.player_name[self.player]) + self.player, self.player_name) self.options.remote_items.value = Toggle.option_true if self.options.goal == Goal.option_legendary_hunt: # Prevent turning off all legendary encounters if len(self.options.allowed_legendary_hunt_encounters.value) == 0: - raise OptionError(f"Pokemon Emerald: Player {self.player} ({self.multiworld.player_name[self.player]}) " - "needs to allow at least one legendary encounter when goal is legendary hunt.") + raise OptionError(f"Pokemon Emerald: Player {self.player} ({self.player_name}) needs to allow at " + "least one legendary encounter when goal is legendary hunt.") # Prevent setting the number of required legendaries higher than the number of enabled legendaries if self.options.legendary_hunt_count.value > len(self.options.allowed_legendary_hunt_encounters.value): logging.warning("Pokemon Emerald: Legendary hunt count for Player %s (%s) higher than number of allowed " "legendary encounters. Reducing to number of allowed encounters.", self.player, - self.multiworld.player_name[self.player]) + self.player_name) self.options.legendary_hunt_count.value = len(self.options.allowed_legendary_hunt_encounters.value) # Require random wild encounters if dexsanity is enabled if self.options.dexsanity and self.options.wild_pokemon == RandomizeWildPokemon.option_vanilla: - raise OptionError(f"Pokemon Emerald: Player {self.player} ({self.multiworld.player_name[self.player]}) must " - "not leave wild encounters vanilla if enabling dexsanity.") + raise OptionError(f"Pokemon Emerald: Player {self.player} ({self.player_name}) must not leave wild " + "encounters vanilla if enabling dexsanity.") # If badges or HMs are vanilla, Norman locks you from using Surf, # which means you're not guaranteed to be able to reach Fortree Gym, @@ -223,7 +223,7 @@ def generate_early(self) -> None: if self.options.norman_count.value > max_norman_count: logging.warning("Pokemon Emerald: Norman requirements for Player %s (%s) are unsafe in combination with " - "other settings. Reducing to 4.", self.player, self.multiworld.get_player_name(self.player)) + "other settings. Reducing to 4.", self.player, self.player_name) self.options.norman_count.value = max_norman_count def create_regions(self) -> None: @@ -588,7 +588,7 @@ def generate_output(self, output_directory: str) -> None: randomize_opponent_parties(self) randomize_starters(self) - patch = PokemonEmeraldProcedurePatch(player=self.player, player_name=self.multiworld.player_name[self.player]) + patch = PokemonEmeraldProcedurePatch(player=self.player, player_name=self.player_name) patch.write_file("base_patch.bsdiff4", pkgutil.get_data(__name__, "data/base_patch.bsdiff4")) write_tokens(self, patch) @@ -607,7 +607,7 @@ def write_spoiler(self, spoiler_handle: TextIO): if self.options.dexsanity: from collections import defaultdict - spoiler_handle.write(f"\n\nWild Pokemon ({self.multiworld.player_name[self.player]}):\n\n") + spoiler_handle.write(f"\n\nWild Pokemon ({self.player_name}):\n\n") species_maps = defaultdict(set) for map in self.modified_maps.values(): @@ -669,7 +669,7 @@ def extend_hint_information(self, hint_data): def modify_multidata(self, multidata: Dict[str, Any]): import base64 - multidata["connect_names"][base64.b64encode(self.auth).decode("ascii")] = multidata["connect_names"][self.multiworld.player_name[self.player]] + multidata["connect_names"][base64.b64encode(self.auth).decode("ascii")] = multidata["connect_names"][self.player_name] def fill_slot_data(self) -> Dict[str, Any]: slot_data = self.options.as_dict( diff --git a/worlds/pokemon_emerald/client.py b/worlds/pokemon_emerald/client.py index 3a99a09acb3e..a830957e9c7e 100644 --- a/worlds/pokemon_emerald/client.py +++ b/worlds/pokemon_emerald/client.py @@ -452,7 +452,7 @@ async def handle_death_link(self, ctx: "BizHawkClientContext", guards: Dict[str, self.death_counter = times_whited_out elif times_whited_out > self.death_counter: await ctx.send_death(f"{ctx.player_names[ctx.slot]} is out of usable POKéMON! " - f"{ctx.player_names[ctx.slot]} whited out!") + f"{ctx.player_names[ctx.slot]} whited out!") self.ignore_next_death_link = True self.death_counter = times_whited_out diff --git a/worlds/pokemon_emerald/rom.py b/worlds/pokemon_emerald/rom.py index 09203bab8fc6..968a103ccd25 100644 --- a/worlds/pokemon_emerald/rom.py +++ b/worlds/pokemon_emerald/rom.py @@ -184,7 +184,7 @@ def write_tokens(world: "PokemonEmeraldWorld", patch: PokemonEmeraldProcedurePat location.item.name ) for trainer in alternates) - player_name_ids: Dict[str, int] = {world.multiworld.player_name[world.player]: 0} + player_name_ids: Dict[str, int] = {world.player_name: 0} item_name_offsets: Dict[str, int] = {} next_item_name_offset = 0 for i, (flag, item_player, item_name) in enumerate(sorted(location_info, key=lambda t: t[0])): @@ -208,7 +208,7 @@ def write_tokens(world: "PokemonEmeraldWorld", patch: PokemonEmeraldProcedurePat struct.pack(" Date: Sat, 1 Jun 2024 13:13:00 +0200 Subject: [PATCH 45/64] The Witness: Put Treehouse Both Orange Bridges EP on the normal EPs exclusion list (#3308) --- worlds/witness/data/settings/EP_Shuffle/EP_Easy.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/worlds/witness/data/settings/EP_Shuffle/EP_Easy.txt b/worlds/witness/data/settings/EP_Shuffle/EP_Easy.txt index 6f9c80fc0a94..95c1fc39fb7a 100644 --- a/worlds/witness/data/settings/EP_Shuffle/EP_Easy.txt +++ b/worlds/witness/data/settings/EP_Shuffle/EP_Easy.txt @@ -15,3 +15,4 @@ Disabled Locations: 0x09D63 (Mountain Pink Bridge EP) 0x09D5E (Mountain Blue Bridge EP) 0x09D5D (Mountain Yellow Bridge EP) +0x220BD (Both Orange Bridges EP) From f40b10dc97d9b890a42ce49197e534e33a23d979 Mon Sep 17 00:00:00 2001 From: Bryce Wilson Date: Sat, 1 Jun 2024 04:14:40 -0700 Subject: [PATCH 46/64] Pokemon Emerald: Adjust options (#3278) --- worlds/pokemon_emerald/CHANGELOG.md | 2 + worlds/pokemon_emerald/options.py | 304 ++++++++++++++-------------- 2 files changed, 154 insertions(+), 152 deletions(-) diff --git a/worlds/pokemon_emerald/CHANGELOG.md b/worlds/pokemon_emerald/CHANGELOG.md index f0bed1257739..e967b2039b12 100644 --- a/worlds/pokemon_emerald/CHANGELOG.md +++ b/worlds/pokemon_emerald/CHANGELOG.md @@ -5,6 +5,8 @@ - When you blacklist species from wild encounters and turn on dexsanity, blacklisted species are not added as locations and won't show up in the wild. Previously they would be forced to show up exactly once. - Added support for some new autotracking events. +- Updated option descriptions. +- Added `full` alias for `100` on TM and HM compatibility options. ### Fixes diff --git a/worlds/pokemon_emerald/options.py b/worlds/pokemon_emerald/options.py index 978f9d3dcdc9..e05b5d96ac74 100644 --- a/worlds/pokemon_emerald/options.py +++ b/worlds/pokemon_emerald/options.py @@ -3,7 +3,7 @@ """ from dataclasses import dataclass -from Options import (Choice, DeathLink, DefaultOnToggle, TextChoice, OptionSet, NamedRange, Range, Toggle, FreeText, +from Options import (Choice, DeathLink, DefaultOnToggle, OptionSet, NamedRange, Range, Toggle, FreeText, PerGameCommonOptions) from .data import data @@ -11,12 +11,12 @@ class Goal(Choice): """ - Determines what your goal is to consider the game beaten + Determines what your goal is to consider the game beaten. - Champion: Become the champion and enter the hall of fame - Steven: Defeat Steven in Meteor Falls - Norman: Defeat Norman in Petalburg Gym - Legendary Hunt: Defeat or catch legendary pokemon (or whatever was randomized into their encounters) + - Champion: Become the champion and enter the hall of fame + - Steven: Defeat Steven in Meteor Falls + - Norman: Defeat Norman in Petalburg Gym + - Legendary Hunt: Defeat or catch legendary pokemon (or whatever was randomized into their encounters) """ display_name = "Goal" default = 0 @@ -28,11 +28,11 @@ class Goal(Choice): class RandomizeBadges(Choice): """ - Adds Badges to the pool + Adds Badges to the pool. - Vanilla: Gym leaders give their own badge - Shuffle: Gym leaders give a random badge - Completely Random: Badges can be found anywhere + - Vanilla: Gym leaders give their own badge + - Shuffle: Gym leaders give a random badge + - Completely Random: Badges can be found anywhere """ display_name = "Randomize Badges" default = 2 @@ -43,11 +43,11 @@ class RandomizeBadges(Choice): class RandomizeHms(Choice): """ - Adds HMs to the pool + Adds HMs to the pool. - Vanilla: HMs are at their vanilla locations - Shuffle: HMs are shuffled among vanilla HM locations - Completely Random: HMs can be found anywhere + - Vanilla: HMs are at their vanilla locations + - Shuffle: HMs are shuffled among vanilla HM locations + - Completely Random: HMs can be found anywhere """ display_name = "Randomize HMs" default = 2 @@ -58,50 +58,51 @@ class RandomizeHms(Choice): class RandomizeKeyItems(DefaultOnToggle): """ - Adds most key items to the pool. These are usually required to unlock - a location or region (e.g. Devon Scope, Letter, Basement Key) + Adds most key items to the pool. + + These are usually required to unlock a location or region (e.g. Devon Scope, Letter, Basement Key). """ display_name = "Randomize Key Items" class RandomizeBikes(Toggle): """ - Adds the mach bike and acro bike to the pool + Adds the Mach Bike and Acro Bike to the pool. """ display_name = "Randomize Bikes" class RandomizeEventTickets(Toggle): """ - Adds the event tickets to the pool, which let you access legendaries by sailing from Lilycove + Adds the event tickets to the pool, which let you access legendaries by sailing from Lilycove. """ display_name = "Randomize Event Tickets" class RandomizeRods(Toggle): """ - Adds fishing rods to the pool + Adds fishing rods to the pool. """ display_name = "Randomize Fishing Rods" class RandomizeOverworldItems(DefaultOnToggle): """ - Adds items on the ground with a Pokeball sprite to the pool + Adds items on the ground with a Pokeball sprite to the pool. """ display_name = "Randomize Overworld Items" class RandomizeHiddenItems(Toggle): """ - Adds hidden items to the pool + Adds hidden items to the pool. """ display_name = "Randomize Hidden Items" class RandomizeNpcGifts(Toggle): """ - Adds most gifts received from NPCs to the pool (not including key items or HMs) + Adds most gifts received from NPCs to the pool (not including key items or HMs). """ display_name = "Randomize NPC Gifts" @@ -115,7 +116,9 @@ class RandomizeBerryTrees(Toggle): class Dexsanity(Toggle): """ - Adding a "caught" pokedex entry gives you an item (catching, evolving, trading, etc.). + Adding a "caught" pokedex entry gives you an item (catching, evolving, trading, etc.). Only wild encounters are considered logical access to a species. + + Blacklisting wild encounters removes the dexsanity location. Defeating gym leaders provides dex info, allowing you to see where on the map you can catch species you need. @@ -126,21 +129,20 @@ class Dexsanity(Toggle): class Trainersanity(Toggle): """ - Defeating a trainer for the first time gives you an item. Trainers are no longer missable. + Defeating a trainer gives you an item. - Trainers no longer give you money for winning. Each trainer adds a valuable item (nugget, stardust, etc.) to the pool. + Trainers are no longer missable. Trainers no longer give you money for winning. Each trainer adds a valuable item (Nugget, Stardust, etc.) to the pool. """ display_name = "Trainersanity" class ItemPoolType(Choice): """ - Determines which non-progression items get put into the item pool + Determines which non-progression items get put into the item pool. - Shuffled: Item pool consists of shuffled vanilla items - Diverse Balanced: Item pool consists of random items approximately proportioned - according to what they're replacing (i.e. more pokeballs, fewer X items, etc.) - Diverse: Item pool consists of uniformly random (non-unique) items + - Shuffled: Item pool consists of shuffled vanilla items + - Diverse Balanced: Item pool consists of random items approximately proportioned according to what they're replacing + - Diverse: Item pool consists of uniformly random (non-unique) items """ display_name = "Item Pool Type" default = 0 @@ -151,14 +153,14 @@ class ItemPoolType(Choice): class HiddenItemsRequireItemfinder(DefaultOnToggle): """ - The Itemfinder is logically required to pick up hidden items + The Itemfinder is logically required to pick up hidden items. """ display_name = "Require Itemfinder" class DarkCavesRequireFlash(Choice): """ - Determines whether HM05 Flash is logically required to navigate a dark cave + Determines whether HM05 Flash is logically required to navigate a dark cave. """ display_name = "Require Flash" default = 3 @@ -170,10 +172,10 @@ class DarkCavesRequireFlash(Choice): class EliteFourRequirement(Choice): """ - Sets the requirements to challenge the elite four + Sets the requirements to challenge the elite four. - Badges: Obtain some number of badges - Gyms: Defeat some number of gyms + - Badges: Obtain some number of badges + - Gyms: Defeat some number of gyms """ display_name = "Elite Four Requirement" default = 0 @@ -183,7 +185,7 @@ class EliteFourRequirement(Choice): class EliteFourCount(Range): """ - Sets the number of badges/gyms required to challenge the elite four + Sets the number of badges/gyms required to challenge the elite four. """ display_name = "Elite Four Count" range_start = 0 @@ -193,10 +195,10 @@ class EliteFourCount(Range): class NormanRequirement(Choice): """ - Sets the requirements to challenge the Petalburg Gym + Sets the requirements to challenge the Petalburg Gym. - Badges: Obtain some number of badges - Gyms: Defeat some number of gyms + - Badges: Obtain some number of badges + - Gyms: Defeat some number of gym leaders """ display_name = "Norman Requirement" default = 0 @@ -206,7 +208,7 @@ class NormanRequirement(Choice): class NormanCount(Range): """ - Sets the number of badges/gyms required to challenge the Petalburg Gym + Sets the number of badges/gyms required to challenge the Petalburg Gym. """ display_name = "Norman Count" range_start = 0 @@ -216,14 +218,16 @@ class NormanCount(Range): class LegendaryHuntCatch(Toggle): """ - Sets whether legendaries need to be caught to satisfy the Legendary Hunt win condition. Defeated legendaries can be respawned by defeating the Elite 4. + Sets whether legendaries need to be caught to satisfy the Legendary Hunt win condition. + + Defeated legendaries can be respawned by defeating the Elite 4. """ display_name = "Legendary Hunt Requires Catching" class LegendaryHuntCount(Range): """ - Sets the number of legendaries that must be caught/defeated for the Legendary Hunt goal + Sets the number of legendaries that must be caught/defeated for the Legendary Hunt goal. """ display_name = "Legendary Hunt Count" range_start = 1 @@ -235,24 +239,12 @@ class AllowedLegendaryHuntEncounters(OptionSet): """ Sets which legendary encounters can contribute to the Legendary Hunt goal. - Latios will always be the roamer. Latias will always be at Southern Island. - - Possible values are: - "Groudon" - "Kyogre" - "Rayquaza" - "Latios" - "Latias" - "Regirock" - "Registeel" - "Regice" - "Ho-Oh" - "Lugia" - "Deoxys" - "Mew" + Latias will always be at Southern Island. Latios will always be the roamer. The TV broadcast describing the roamer gives you "seen" info for Latios. + + The braille puzzle in Sealed Chamber gives you "seen" info for Wailord and Relicanth. The move tutor in Fortree City always teaches Dig. """ display_name = "Allowed Legendary Hunt Encounters" - valid_keys = frozenset([ + valid_keys = [ "Groudon", "Kyogre", "Rayquaza", @@ -265,19 +257,19 @@ class AllowedLegendaryHuntEncounters(OptionSet): "Lugia", "Deoxys", "Mew", - ]) + ] default = valid_keys.copy() class RandomizeWildPokemon(Choice): """ - Randomizes wild pokemon encounters (grass, caves, water, fishing) + Randomizes wild pokemon encounters (grass, caves, water, fishing). - Vanilla: Wild encounters are unchanged - Match Base Stats: Wild pokemon are replaced with species with approximately the same bst - Match Type: Wild pokemon are replaced with species that share a type with the original - Match Base Stats and Type: Apply both Match Base Stats and Match Type - Completely Random: There are no restrictions + - Vanilla: Wild encounters are unchanged + - Match Base Stats: Wild pokemon are replaced with species with approximately the same bst + - Match Type: Wild pokemon are replaced with species that share a type with the original + - Match Base Stats and Type: Apply both Match Base Stats and Match Type + - Completely Random: There are no restrictions """ display_name = "Randomize Wild Pokemon" default = 0 @@ -294,21 +286,21 @@ class WildEncounterBlacklist(OptionSet): May be overridden if enforcing other restrictions in combination with this blacklist is impossible. - Use "_Legendaries" as a shortcut for legendary pokemon. + Use "_Legendaries" as a shortcut for all legendary pokemon. """ display_name = "Wild Encounter Blacklist" - valid_keys = frozenset(species.label for species in data.species.values()) | {"_Legendaries"} + valid_keys = ["_Legendaries"] + sorted([species.label for species in data.species.values()]) class RandomizeStarters(Choice): """ - Randomizes the starter pokemon in Professor Birch's bag + Randomizes the starter pokemon in Professor Birch's bag. - Vanilla: Starters are unchanged - Match Base Stats: Starters are replaced with species with approximately the same bst - Match Type: Starters are replaced with species that share a type with the original - Match Base Stats and Type: Apply both Match Base Stats and Match Type - Completely Random: There are no restrictions + - Vanilla: Starters are unchanged + - Match Base Stats: Starters are replaced with species with approximately the same bst + - Match Type: Starters are replaced with species that share a type with the original + - Match Base Stats and Type: Apply both Match Base Stats and Match Type + - Completely Random: There are no restrictions """ display_name = "Randomize Starters" default = 0 @@ -325,21 +317,21 @@ class StarterBlacklist(OptionSet): May be overridden if enforcing other restrictions in combination with this blacklist is impossible. - Use "_Legendaries" as a shortcut for legendary pokemon. + Use "_Legendaries" as a shortcut for all legendary pokemon. """ display_name = "Starter Blacklist" - valid_keys = frozenset(species.label for species in data.species.values()) | {"_Legendaries"} + valid_keys = ["_Legendaries"] + sorted([species.label for species in data.species.values()]) class RandomizeTrainerParties(Choice): """ Randomizes the parties of all trainers. - Vanilla: Parties are unchanged - Match Base Stats: Trainer pokemon are replaced with species with approximately the same bst - Match Type: Trainer pokemon are replaced with species that share a type with the original - Match Base Stats and Type: Apply both Match Base Stats and Match Type - Completely Random: There are no restrictions + - Vanilla: Parties are unchanged + - Match Base Stats: Trainer pokemon are replaced with species with approximately the same bst + - Match Type: Trainer pokemon are replaced with species that share a type with the original + - Match Base Stats and Type: Apply both Match Base Stats and Match Type + - Completely Random: There are no restrictions """ display_name = "Randomize Trainer Parties" default = 0 @@ -356,10 +348,10 @@ class TrainerPartyBlacklist(OptionSet): May be overridden if enforcing other restrictions in combination with this blacklist is impossible. - Use "_Legendaries" as a shortcut for legendary pokemon. + Use "_Legendaries" as a shortcut for all legendary pokemon. """ display_name = "Trainer Party Blacklist" - valid_keys = frozenset(species.label for species in data.species.values()) | {"_Legendaries"} + valid_keys = ["_Legendaries"] + sorted([species.label for species in data.species.values()]) class ForceFullyEvolved(Range): @@ -376,12 +368,12 @@ class RandomizeLegendaryEncounters(Choice): """ Randomizes legendary encounters (Rayquaza, Regice, Latias, etc.). The roamer will always be Latios during legendary hunts. - Vanilla: Legendary encounters are unchanged - Shuffle: Legendary encounters are shuffled between each other - Match Base Stats: Legendary encounters are replaced with species with approximately the same bst - Match Type: Legendary encounters are replaced with species that share a type with the original - Match Base Stats and Type: Apply both Match Base Stats and Match Type - Completely Random: There are no restrictions + - Vanilla: Legendary encounters are unchanged + - Shuffle: Legendary encounters are shuffled between each other + - Match Base Stats: Legendary encounters are replaced with species with approximately the same bst + - Match Type: Legendary encounters are replaced with species that share a type with the original + - Match Base Stats and Type: Apply both Match Base Stats and Match Type + - Completely Random: There are no restrictions """ display_name = "Randomize Legendary Encounters" default = 0 @@ -397,12 +389,12 @@ class RandomizeMiscPokemon(Choice): """ Randomizes non-legendary static encounters. May grow to include other pokemon like trades or gifts. - Vanilla: Species are unchanged - Shuffle: Species are shuffled between each other - Match Base Stats: Species are replaced with species with approximately the same bst - Match Type: Species are replaced with species that share a type with the original - Match Base Stats and Type: Apply both Match Base Stats and Match Type - Completely Random: There are no restrictions + - Vanilla: Species are unchanged + - Shuffle: Species are shuffled between each other + - Match Base Stats: Species are replaced with species with approximately the same bst + - Match Type: Species are replaced with species that share a type with the original + - Match Base Stats and Type: Apply both Match Base Stats and Match Type + - Completely Random: There are no restrictions """ display_name = "Randomize Misc Pokemon" default = 0 @@ -418,10 +410,10 @@ class RandomizeTypes(Choice): """ Randomizes the type(s) of every pokemon. Each species will have the same number of types. - Vanilla: Types are unchanged - Shuffle: Types are shuffled globally for all species (e.g. every Water-type pokemon becomes Fire-type) - Completely Random: Each species has its type(s) randomized - Follow Evolutions: Types are randomized per evolution line instead of per species + - Vanilla: Types are unchanged + - Shuffle: Types are shuffled globally for all species (e.g. every Water-type pokemon becomes Fire-type) + - Completely Random: Each species has its type(s) randomized + - Follow Evolutions: Types are randomized per evolution line instead of per species """ display_name = "Randomize Types" default = 0 @@ -435,10 +427,9 @@ class RandomizeAbilities(Choice): """ Randomizes abilities of every species. Each species will have the same number of abilities. - Vanilla: Abilities are unchanged - Completely Random: Each species has its abilities randomized - Follow Evolutions: Abilities are randomized, but if a pokemon would normally retain its ability - when evolving, the random ability will also be retained + - Vanilla: Abilities are unchanged + - Completely Random: Each species has its abilities randomized + - Follow Evolutions: Abilities are randomized, but if a pokemon would normally retain its ability when evolving, the random ability will also be retained """ display_name = "Randomize Abilities" default = 0 @@ -449,22 +440,21 @@ class RandomizeAbilities(Choice): class AbilityBlacklist(OptionSet): """ - A list of abilities which no pokemon should have if abilities are randomized. - For example, you could exclude Wonder Guard and Arena Trap like this: - ["Wonder Guard", "Arena Trap"] + Prevent species from being given these abilities. + + Has no effect if abilities are not randomized. """ display_name = "Ability Blacklist" - valid_keys = frozenset([ability.label for ability in data.abilities]) + valid_keys = sorted([ability.label for ability in data.abilities]) class LevelUpMoves(Choice): """ - Randomizes the moves a pokemon learns when they reach a level where they would learn a move. - Your starter is guaranteed to have a usable damaging move. + Randomizes the moves a pokemon learns when they reach a level where they would learn a move. Your starter is guaranteed to have a usable damaging move. - Vanilla: Learnset is unchanged - Randomized: Moves are randomized - Start with Four Moves: Moves are randomized and all Pokemon know 4 moves at level 1 + - Vanilla: Learnset is unchanged + - Randomized: Moves are randomized + - Start with Four Moves: Moves are randomized and all Pokemon know 4 moves at level 1 """ display_name = "Level Up Moves" default = 0 @@ -487,8 +477,7 @@ class MoveMatchTypeBias(Range): class MoveNormalTypeBias(Range): """ - After it has been decided that a move will not be forced to match types, sets the probability that a learned move - will be forced to be the Normal type. + After it has been decided that a move will not be forced to match types, sets the probability that a learned move will be forced to be the Normal type. If a move is not forced to be Normal, it will be completely random. """ @@ -500,41 +489,51 @@ class MoveNormalTypeBias(Range): class MoveBlacklist(OptionSet): """ - A list of moves which should be excluded from learnsets, TMs, and move tutors. + Prevents species from learning these moves via learnsets, TMs, and move tutors. + + HM moves are already banned. """ display_name = "Move Blacklist" - valid_keys = frozenset(data.move_labels.keys()) + valid_keys = sorted(data.move_labels.keys()) class HmCompatibility(NamedRange): """ - Sets the percent chance that a given HM is compatible with a species + Sets the percent chance that a given HM is compatible with a species. + + Some opponents like gym leaders are allowed to use HMs. This option can affect the moves they know. """ display_name = "HM Compatibility" default = -1 range_start = 50 range_end = 100 special_range_names = { - "vanilla": -1 + "vanilla": -1, + "full": 100, } class TmTutorCompatibility(NamedRange): """ - Sets the percent chance that a given TM or move tutor is compatible with a species + Sets the percent chance that a given TM or move tutor is compatible with a species. + + Some opponents like gym leaders are allowed to use TMs. This option can affect the moves they know. """ display_name = "TM/Tutor Compatibility" default = -1 range_start = 0 range_end = 100 special_range_names = { - "vanilla": -1 + "vanilla": -1, + "full": 100, } class TmTutorMoves(Toggle): """ - Randomizes the moves taught by TMs and move tutors + Randomizes the moves taught by TMs and move tutors. + + Some opponents like gym leaders are allowed to use TMs. This option can affect the moves they know. """ display_name = "TM/Tutor Moves" @@ -562,7 +561,7 @@ class MinCatchRate(Range): class GuaranteedCatch(Toggle): """ - Every throw is guaranteed to catch a wild pokemon + Every throw is guaranteed to catch a wild pokemon. """ display_name = "Guaranteed Catch" @@ -571,14 +570,16 @@ class NormalizeEncounterRates(Toggle): """ Make every slot on an encounter table approximately equally likely. - This does NOT mean every species is equally likely. But it will make rarer encounters less rare overall. + This does NOT mean each species is equally likely. In the vanilla game, each species may occupy more than one slot, and slots vary in probability. + + Species will still occupy the same slots as vanilla, but the slots will be equally weighted. The minimum encounter rate will be 8% (higher in water). """ display_name = "Normalize Encounter Rates" class ExpModifier(Range): """ - Multiplies gained experience by a percentage + Multiplies gained experience by a percentage. 100 is default 50 is half @@ -593,14 +594,14 @@ class ExpModifier(Range): class BlindTrainers(Toggle): """ - Causes trainers to not start a battle with you unless you talk to them + Trainers will not start a battle with you unless you talk to them. """ display_name = "Blind Trainers" class PurgeSpinners(Toggle): """ - Trainers will rotate in predictable patterns on a set interval instead of randomly and don't turn toward you when you run + Trainers will rotate in predictable patterns on a set interval instead of randomly and don't turn toward you when you run. """ display_name = "Purge Spinners" @@ -613,9 +614,9 @@ class MatchTrainerLevels(Choice): This is a pseudo-replacement for a level cap and makes every trainer battle a fair fight while still allowing you to level up. - Off: The vanilla experience - Additive: The modifier you apply to your team is a flat bonus - Multiplicative: The modifier you apply to your team is a percent bonus + - Off: The vanilla experience + - Additive: The modifier you apply to your team is a flat bonus + - Multiplicative: The modifier you apply to your team is a percent bonus """ display_name = "Match Trainer Levels" default = 0 @@ -629,10 +630,10 @@ class MatchTrainerLevelsBonus(Range): A level bonus (or penalty) to apply to your team when matching an opponent's levels. When the match trainer levels option is "additive", this value is added to your team's levels during a battle. - For example, if this value is 5 (+5 levels), you'll have a level 25 team against a level 20 team, and a level 45 team against a level 40 team. + For example, if this value is 5 (+5 levels), you'll have a level 25 team against a level 20 team, and a level 45 team against a level 40 team. When the match trainer levels option is "multiplicative", this is a percent bonus. - For example, if this value is 5 (+5%), you'll have a level 21 team against a level 20 team, and a level 42 team against a level 40 team. + For example, if this value is 5 (+5%), you'll have a level 21 team against a level 20 team, and a level 42 team against a level 40 team. """ display_name = "Match Trainer Levels Modifier" range_start = -100 @@ -643,7 +644,9 @@ class MatchTrainerLevelsBonus(Range): class DoubleBattleChance(Range): """ The percent chance that a trainer with more than 1 pokemon will be converted into a double battle. + If these trainers would normally approach you, they will only do so if you have 2 unfainted pokemon. + They can be battled by talking to them no matter what. """ display_name = "Double Battle Chance" @@ -654,7 +657,7 @@ class DoubleBattleChance(Range): class BetterShops(Toggle): """ - Pokemarts sell every item that can be obtained in a pokemart (except mail, which is still unique to the relevant city) + Pokemarts sell every item that can be obtained in a pokemart (except mail, which is still unique to the relevant city). """ display_name = "Better Shops" @@ -663,19 +666,10 @@ class RemoveRoadblocks(OptionSet): """ Removes specific NPCs that normally stand in your way until certain events are completed. - This can open up the world a bit and make your playthrough less linear, but careful how many you remove; it may make too much of your world accessible upon receiving Surf. - - Possible values are: - "Route 110 Aqua Grunts" - "Route 112 Magma Grunts" - "Route 119 Aqua Grunts" - "Safari Zone Construction Workers" - "Lilycove City Wailmer" - "Aqua Hideout Grunts" - "Seafloor Cavern Aqua Grunt" + This can open up the world a bit and make your playthrough less linear, but be careful how many you remove; it may make too much of your world accessible upon receiving Surf. """ display_name = "Remove Roadblocks" - valid_keys = frozenset([ + valid_keys = [ "Route 110 Aqua Grunts", "Route 112 Magma Grunts", "Route 119 Aqua Grunts", @@ -683,12 +677,13 @@ class RemoveRoadblocks(OptionSet): "Lilycove City Wailmer", "Aqua Hideout Grunts", "Seafloor Cavern Aqua Grunt", - ]) + ] class ExtraBoulders(Toggle): """ Places strength boulders on Route 115 which block access to Meteor Falls from the beach. + This aims to take some power away from Surf by restricting how much it allows you to access. """ display_name = "Extra Boulders" @@ -697,6 +692,7 @@ class ExtraBoulders(Toggle): class ExtraBumpySlope(Toggle): """ Adds a bumpy slope to Route 115 which allows access to Meteor Falls if you have the Acro Bike. + This aims to take some power away from Surf by adding a new way to exit the Rustboro area. """ display_name = "Extra Bumpy Slope" @@ -705,6 +701,7 @@ class ExtraBumpySlope(Toggle): class ModifyRoute118(Toggle): """ Changes the layout of Route 118 so that it must be crossed with the Acro Bike instead of Surf. + This aims to take some power away from Surf by restricting how much it allows you to access. """ display_name = "Modify Route 118" @@ -712,14 +709,14 @@ class ModifyRoute118(Toggle): class FreeFlyLocation(Toggle): """ - Enables flying to one random location when Mom gives you the running shoes (excluding cities reachable with no items) + Enables flying to one random location (excluding cities reachable with no items). """ display_name = "Free Fly Location" -class HmRequirements(TextChoice): +class HmRequirements(Choice): """ - Sets the requirements to use HMs outside of battle + Sets the requirements to use HMs outside of battle. """ display_name = "HM Requirements" default = 0 @@ -729,7 +726,7 @@ class HmRequirements(TextChoice): class TurboA(Toggle): """ - Holding A will advance most text automatically + Holding A will advance most text automatically. """ display_name = "Turbo A" @@ -738,9 +735,9 @@ class ReceiveItemMessages(Choice): """ Determines whether you receive an in-game notification when receiving an item. Items can still only be received in the overworld. - All: Every item shows a message - Progression: Only progression items show a message - None: All items are added to your bag silently (badges will still show) + - All: Every item shows a message + - Progression: Only progression items show a message + - None: All items are added to your bag silently (badges will still show). """ display_name = "Receive Item Messages" default = 0 @@ -754,6 +751,7 @@ class RemoteItems(Toggle): Instead of placing your own items directly into the ROM, all items are received from the server, including items you find for yourself. This enables co-op of a single slot and recovering more items after a lost save file (if you're so unlucky). + But it changes pickup behavior slightly and requires connection to the server to receive any items. """ display_name = "Remote Items" @@ -781,9 +779,10 @@ class WonderTrading(DefaultOnToggle): Wonder trading NEVER affects logic. - Certain aspects of a pokemon species are per-game, not per-pokemon. - As a result, some things are not retained during a trade, including type, ability, level up learnset, and so on. + Certain aspects of a pokemon species are per-game, not per-pokemon. As a result, some things are not retained during a trade, including type, ability, level up learnset, and so on. + Receiving a pokemon this way does not mark it as found in your pokedex. + Trade evolutions do not evolve this way; they retain their modified methods (level ups and item use). """ display_name = "Wonder Trading" @@ -795,6 +794,7 @@ class EasterEgg(FreeText): All secret phrases are something that could be a trendy phrase in Dewford Town. They are case insensitive. """ + display_name = "Easter Egg" default = "EMERALD SECRET" From 4e5b6bb3d23277aad83865f2c909e627ca64f2c2 Mon Sep 17 00:00:00 2001 From: Silvris <58583688+Silvris@users.noreply.github.com> Date: Sat, 1 Jun 2024 06:34:41 -0500 Subject: [PATCH 47/64] Core: move PlandoConnections and PlandoTexts to the options system (#2904) Co-authored-by: Doug Hoskisson Co-authored-by: Scipio Wright Co-authored-by: beauxq Co-authored-by: alwaysintreble --- Generate.py | 29 +- Options.py | 226 +++++++++++++++- worlds/alttp/Options.py | 27 +- worlds/alttp/Rom.py | 4 +- worlds/alttp/Shops.py | 10 +- worlds/alttp/Text.py | 409 +++++++++++++++++++++++++++++ worlds/generic/__init__.py | 6 - worlds/kdl3/Options.py | 7 +- worlds/kdl3/Regions.py | 4 +- worlds/kdl3/test/__init__.py | 3 +- worlds/kdl3/test/test_locations.py | 6 +- worlds/messenger/options.py | 37 ++- worlds/messenger/portals.py | 9 +- worlds/minecraft/Options.py | 15 +- worlds/oot/Options.py | 9 +- worlds/oot/__init__.py | 4 +- worlds/tunic/__init__.py | 14 +- worlds/tunic/er_scripts.py | 4 +- worlds/tunic/options.py | 15 +- 19 files changed, 767 insertions(+), 71 deletions(-) diff --git a/Generate.py b/Generate.py index fab34c893ae9..67988bf8b30d 100644 --- a/Generate.py +++ b/Generate.py @@ -23,9 +23,7 @@ from settings import get_settings from Utils import parse_yamls, version_tuple, __version__, tuplize_version from worlds.alttp.EntranceRandomizer import parse_arguments -from worlds.alttp.Text import TextTable from worlds.AutoWorld import AutoWorldRegister -from worlds.generic import PlandoConnection from worlds import failed_world_loads @@ -506,35 +504,12 @@ def roll_settings(weights: dict, plando_options: PlandoOptions = PlandoOptions.b if PlandoOptions.items in plando_options: ret.plando_items = game_weights.get("plando_items", []) if ret.game == "A Link to the Past": - roll_alttp_settings(ret, game_weights, plando_options) - if PlandoOptions.connections in plando_options: - ret.plando_connections = [] - options = game_weights.get("plando_connections", []) - for placement in options: - if roll_percentage(get_choice("percentage", placement, 100)): - ret.plando_connections.append(PlandoConnection( - get_choice("entrance", placement), - get_choice("exit", placement), - get_choice("direction", placement, "both") - )) + roll_alttp_settings(ret, game_weights) return ret -def roll_alttp_settings(ret: argparse.Namespace, weights, plando_options): - - ret.plando_texts = {} - if PlandoOptions.texts in plando_options: - tt = TextTable() - tt.removeUnwantedText() - options = weights.get("plando_texts", []) - for placement in options: - if roll_percentage(get_choice_legacy("percentage", placement, 100)): - at = str(get_choice_legacy("at", placement)) - if at not in tt: - raise Exception(f"No text target \"{at}\" found.") - ret.plando_texts[at] = str(get_choice_legacy("text", placement)) - +def roll_alttp_settings(ret: argparse.Namespace, weights): ret.sprite_pool = weights.get('sprite_pool', []) ret.sprite = get_choice_legacy('sprite', weights, "Link") if 'random_sprite_on_event' in weights: diff --git a/Options.py b/Options.py index 995473243959..40a6996d325a 100644 --- a/Options.py +++ b/Options.py @@ -12,6 +12,7 @@ from dataclasses import dataclass from schema import And, Optional, Or, Schema +from typing_extensions import Self from Utils import get_fuzzy_results, is_iterable_except_str @@ -896,6 +897,228 @@ class ItemSet(OptionSet): convert_name_groups = True +class PlandoText(typing.NamedTuple): + at: str + text: typing.List[str] + percentage: int = 100 + + +PlandoTextsFromAnyType = typing.Union[ + typing.Iterable[typing.Union[typing.Mapping[str, typing.Any], PlandoText, typing.Any]], typing.Any +] + + +class PlandoTexts(Option[typing.List[PlandoText]], VerifyKeys): + default = () + supports_weighting = False + display_name = "Plando Texts" + + def __init__(self, value: typing.Iterable[PlandoText]) -> None: + self.value = list(deepcopy(value)) + super().__init__() + + def verify(self, world: typing.Type[World], player_name: str, plando_options: "PlandoOptions") -> None: + from BaseClasses import PlandoOptions + if self.value and not (PlandoOptions.texts & plando_options): + # plando is disabled but plando options were given so overwrite the options + self.value = [] + logging.warning(f"The plando texts module is turned off, " + f"so text for {player_name} will be ignored.") + + @classmethod + def from_any(cls, data: PlandoTextsFromAnyType) -> Self: + texts: typing.List[PlandoText] = [] + if isinstance(data, typing.Iterable): + for text in data: + if isinstance(text, typing.Mapping): + if random.random() < float(text.get("percentage", 100)/100): + at = text.get("at", None) + if at is not None: + given_text = text.get("text", []) + if isinstance(given_text, str): + given_text = [given_text] + texts.append(PlandoText( + at, + given_text, + text.get("percentage", 100) + )) + elif isinstance(text, PlandoText): + if random.random() < float(text.percentage/100): + texts.append(text) + else: + raise Exception(f"Cannot create plando text from non-dictionary type, got {type(text)}") + cls.verify_keys([text.at for text in texts]) + return cls(texts) + else: + raise NotImplementedError(f"Cannot Convert from non-list, got {type(data)}") + + @classmethod + def get_option_name(cls, value: typing.List[PlandoText]) -> str: + return str({text.at: " ".join(text.text) for text in value}) + + def __iter__(self) -> typing.Iterator[PlandoText]: + yield from self.value + + def __getitem__(self, index: typing.SupportsIndex) -> PlandoText: + return self.value.__getitem__(index) + + def __len__(self) -> int: + return self.value.__len__() + + +class ConnectionsMeta(AssembleOptions): + def __new__(mcs, name: str, bases: tuple[type, ...], attrs: dict[str, typing.Any]): + if name != "PlandoConnections": + assert "entrances" in attrs, f"Please define valid entrances for {name}" + attrs["entrances"] = frozenset((connection.lower() for connection in attrs["entrances"])) + assert "exits" in attrs, f"Please define valid exits for {name}" + attrs["exits"] = frozenset((connection.lower() for connection in attrs["exits"])) + if "__doc__" not in attrs: + attrs["__doc__"] = PlandoConnections.__doc__ + cls = super().__new__(mcs, name, bases, attrs) + return cls + + +class PlandoConnection(typing.NamedTuple): + class Direction: + entrance = "entrance" + exit = "exit" + both = "both" + + entrance: str + exit: str + direction: typing.Literal["entrance", "exit", "both"] # TODO: convert Direction to StrEnum once 3.8 is dropped + percentage: int = 100 + + +PlandoConFromAnyType = typing.Union[ + typing.Iterable[typing.Union[typing.Mapping[str, typing.Any], PlandoConnection, typing.Any]], typing.Any +] + + +class PlandoConnections(Option[typing.List[PlandoConnection]], metaclass=ConnectionsMeta): + """Generic connections plando. Format is: + - entrance: "Entrance Name" + exit: "Exit Name" + direction: "Direction" + percentage: 100 + Direction must be one of 'entrance', 'exit', or 'both', and defaults to 'both' if omitted. + Percentage is an integer from 1 to 100, and defaults to 100 when omitted.""" + + display_name = "Plando Connections" + + default = () + supports_weighting = False + + entrances: typing.ClassVar[typing.AbstractSet[str]] + exits: typing.ClassVar[typing.AbstractSet[str]] + + duplicate_exits: bool = False + """Whether or not exits should be allowed to be duplicate.""" + + def __init__(self, value: typing.Iterable[PlandoConnection]): + self.value = list(deepcopy(value)) + super(PlandoConnections, self).__init__() + + @classmethod + def validate_entrance_name(cls, entrance: str) -> bool: + return entrance.lower() in cls.entrances + + @classmethod + def validate_exit_name(cls, exit: str) -> bool: + return exit.lower() in cls.exits + + @classmethod + def can_connect(cls, entrance: str, exit: str) -> bool: + """Checks that a given entrance can connect to a given exit. + By default, this will always return true unless overridden.""" + return True + + @classmethod + def validate_plando_connections(cls, connections: typing.Iterable[PlandoConnection]) -> None: + used_entrances: typing.List[str] = [] + used_exits: typing.List[str] = [] + for connection in connections: + entrance = connection.entrance + exit = connection.exit + direction = connection.direction + if direction not in (PlandoConnection.Direction.entrance, + PlandoConnection.Direction.exit, + PlandoConnection.Direction.both): + raise ValueError(f"Unknown direction: {direction}") + if entrance in used_entrances: + raise ValueError(f"Duplicate Entrance {entrance} not allowed.") + if not cls.duplicate_exits and exit in used_exits: + raise ValueError(f"Duplicate Exit {exit} not allowed.") + used_entrances.append(entrance) + used_exits.append(exit) + if not cls.validate_entrance_name(entrance): + raise ValueError(f"{entrance.title()} is not a valid entrance.") + if not cls.validate_exit_name(exit): + raise ValueError(f"{exit.title()} is not a valid exit.") + if not cls.can_connect(entrance, exit): + raise ValueError(f"Connection between {entrance.title()} and {exit.title()} is invalid.") + + @classmethod + def from_any(cls, data: PlandoConFromAnyType) -> Self: + if not isinstance(data, typing.Iterable): + raise Exception(f"Cannot create plando connections from non-List value, got {type(data)}.") + + value: typing.List[PlandoConnection] = [] + for connection in data: + if isinstance(connection, typing.Mapping): + percentage = connection.get("percentage", 100) + if random.random() < float(percentage / 100): + entrance = connection.get("entrance", None) + if is_iterable_except_str(entrance): + entrance = random.choice(sorted(entrance)) + exit = connection.get("exit", None) + if is_iterable_except_str(exit): + exit = random.choice(sorted(exit)) + direction = connection.get("direction", "both") + + if not entrance or not exit: + raise Exception("Plando connection must have an entrance and an exit.") + value.append(PlandoConnection( + entrance, + exit, + direction, + percentage + )) + elif isinstance(connection, PlandoConnection): + if random.random() < float(connection.percentage / 100): + value.append(connection) + else: + raise Exception(f"Cannot create connection from non-Dict type, got {type(connection)}.") + cls.validate_plando_connections(value) + return cls(value) + + def verify(self, world: typing.Type[World], player_name: str, plando_options: "PlandoOptions") -> None: + from BaseClasses import PlandoOptions + if self.value and not (PlandoOptions.connections & plando_options): + # plando is disabled but plando options were given so overwrite the options + self.value = [] + logging.warning(f"The plando connections module is turned off, " + f"so connections for {player_name} will be ignored.") + + @classmethod + def get_option_name(cls, value: typing.List[PlandoConnection]) -> str: + return ", ".join(["%s %s %s" % (connection.entrance, + "<=>" if connection.direction == PlandoConnection.Direction.both else + "<=" if connection.direction == PlandoConnection.Direction.exit else + "=>", + connection.exit) for connection in value]) + + def __getitem__(self, index: typing.SupportsIndex) -> PlandoConnection: + return self.value.__getitem__(index) + + def __iter__(self) -> typing.Iterator[PlandoConnection]: + yield from self.value + + def __len__(self) -> int: + return len(self.value) + + class Accessibility(Choice): """Set rules for reachability of your items/locations. Locations: ensure everything can be reached and acquired. @@ -1049,7 +1272,8 @@ class ItemLinks(OptionList): ]) @staticmethod - def verify_items(items: typing.List[str], item_link: str, pool_name: str, world, allow_item_groups: bool = True) -> typing.Set: + def verify_items(items: typing.List[str], item_link: str, pool_name: str, world, + allow_item_groups: bool = True) -> typing.Set: pool = set() for item_name in items: if item_name not in world.item_names and (not allow_item_groups or item_name not in world.item_name_groups): diff --git a/worlds/alttp/Options.py b/worlds/alttp/Options.py index 8cb377b7a44f..11c1a0165b53 100644 --- a/worlds/alttp/Options.py +++ b/worlds/alttp/Options.py @@ -1,8 +1,11 @@ import typing from BaseClasses import MultiWorld -from Options import Choice, Range, Option, Toggle, DefaultOnToggle, DeathLink, StartInventoryPool, PlandoBosses,\ - FreeText, Removed +from Options import Choice, Range, Option, Toggle, DefaultOnToggle, DeathLink, \ + StartInventoryPool, PlandoBosses, PlandoConnections, PlandoTexts, FreeText, Removed +from .EntranceShuffle import default_connections, default_dungeon_connections, \ + inverted_default_connections, inverted_default_dungeon_connections +from .Text import TextTable class GlitchesRequired(Choice): @@ -721,7 +724,27 @@ class AllowCollect(DefaultOnToggle): display_name = "Allow Collection of checks for other players" +class ALttPPlandoConnections(PlandoConnections): + entrances = set([connection[0] for connection in ( + *default_connections, *default_dungeon_connections, *inverted_default_connections, + *inverted_default_dungeon_connections)]) + exits = set([connection[1] for connection in ( + *default_connections, *default_dungeon_connections, *inverted_default_connections, + *inverted_default_dungeon_connections)]) + + +class ALttPPlandoTexts(PlandoTexts): + """Text plando. Format is: + - text: 'This is your text' + at: text_key + percentage: 100 + Percentage is an integer from 1 to 100, and defaults to 100 when omitted.""" + valid_keys = TextTable.valid_keys + + alttp_options: typing.Dict[str, type(Option)] = { + "plando_connections": ALttPPlandoConnections, + "plando_texts": ALttPPlandoTexts, "start_inventory_from_pool": StartInventoryPool, "goal": Goal, "mode": Mode, diff --git a/worlds/alttp/Rom.py b/worlds/alttp/Rom.py index 05113514e484..f451e88fdb2b 100644 --- a/worlds/alttp/Rom.py +++ b/worlds/alttp/Rom.py @@ -2538,12 +2538,12 @@ def hint_text(dest, ped_hint=False): tt['menu_start_2'] = "{MENU}\n{SPEED0}\n≥@'s house\n Dark Chapel\n{CHOICE3}" tt['menu_start_3'] = "{MENU}\n{SPEED0}\n≥@'s house\n Dark Chapel\n Mountain Cave\n{CHOICE2}" - for at, text in world.plando_texts[player].items(): + for at, text, _ in world.plando_texts[player]: if at not in tt: raise Exception(f"No text target \"{at}\" found.") else: - tt[at] = text + tt[at] = "\n".join(text) rom.write_bytes(0xE0000, tt.getBytes()) diff --git a/worlds/alttp/Shops.py b/worlds/alttp/Shops.py index dbe8cc1f9dfa..db2b5b680c1d 100644 --- a/worlds/alttp/Shops.py +++ b/worlds/alttp/Shops.py @@ -9,9 +9,9 @@ from BaseClasses import CollectionState from .SubClasses import ALttPLocation -from .EntranceShuffle import door_addresses + from .Items import item_name_groups -from .Options import small_key_shuffle, RandomizeShopInventories + from .StateHelpers import has_hearts, can_use_bombs, can_hold_arrows logger = logging.getLogger("Shops") @@ -66,6 +66,7 @@ def item_count(self) -> int: return 0 def get_bytes(self) -> List[int]: + from .EntranceShuffle import door_addresses # [id][roomID-low][roomID-high][doorID][zero][shop_config][shopkeeper_config][sram_index] entrances = self.region.entrances config = self.item_count @@ -181,7 +182,7 @@ def push_shop_inventories(multiworld): def create_shops(multiworld, player: int): - + from .Options import RandomizeShopInventories player_shop_table = shop_table.copy() if multiworld.include_witch_hut[player]: player_shop_table["Potion Shop"] = player_shop_table["Potion Shop"]._replace(locked=False) @@ -304,6 +305,7 @@ class ShopData(NamedTuple): def set_up_shops(multiworld, player: int): + from .Options import small_key_shuffle # TODO: move hard+ mode changes for shields here, utilizing the new shops if multiworld.retro_bow[player]: @@ -426,7 +428,7 @@ def get_price_modifier(item): def get_price(multiworld, item, player: int, price_type=None): """Converts a raw Rupee price into a special price type""" - + from .Options import small_key_shuffle if price_type: price_types = [price_type] else: diff --git a/worlds/alttp/Text.py b/worlds/alttp/Text.py index b479a9b8e002..c005cacd8f9f 100644 --- a/worlds/alttp/Text.py +++ b/worlds/alttp/Text.py @@ -1289,6 +1289,415 @@ class LargeCreditBottomMapper(CharTextMapper): class TextTable(object): SIZE = 0x7355 + valid_keys = [ + "set_cursor", + "set_cursor2", + "game_over_menu", + "var_test", + "follower_no_enter", + "choice_1_3", + "choice_2_3", + "choice_3_3", + "choice_1_2", + "choice_2_2", + "uncle_leaving_text", + "uncle_dying_sewer", + "tutorial_guard_1", + "tutorial_guard_2", + "tutorial_guard_3", + "tutorial_guard_4", + "tutorial_guard_5", + "tutorial_guard_6", + "tutorial_guard_7", + "priest_sanctuary_before_leave", + "sanctuary_enter", + "zelda_sanctuary_story", + "priest_sanctuary_before_pendants", + "priest_sanctuary_after_pendants_before_master_sword", + "priest_sanctuary_dying", + "zelda_save_sewers", + "priest_info", + "zelda_sanctuary_before_leave", + "telepathic_intro", + "telepathic_reminder", + "zelda_go_to_throne", + "zelda_push_throne", + "zelda_switch_room_pull", + "zelda_save_lets_go", + "zelda_save_repeat", + "zelda_before_pendants", + "zelda_after_pendants_before_master_sword", + "telepathic_zelda_right_after_master_sword", + "zelda_sewers", + "zelda_switch_room", + "kakariko_saharalasa_wife", + "kakariko_saharalasa_wife_sword_story", + "kakariko_saharalasa_wife_closing", + "kakariko_saharalasa_after_master_sword", + "kakariko_alert_guards", + "sahasrahla_quest_have_pendants", + "sahasrahla_quest_have_master_sword", + "sahasrahla_quest_information", + "sahasrahla_bring_courage", + "sahasrahla_have_ice_rod", + "telepathic_sahasrahla_beat_agahnim", + "telepathic_sahasrahla_beat_agahnim_no_pearl", + "sahasrahla_have_boots_no_icerod", + "sahasrahla_have_courage", + "sahasrahla_found", + "sign_rain_north_of_links_house", + "sign_north_of_links_house", + "sign_path_to_death_mountain", + "sign_lost_woods", + "sign_zoras", + "sign_outside_magic_shop", + "sign_death_mountain_cave_back", + "sign_east_of_links_house", + "sign_south_of_lumberjacks", + "sign_east_of_desert", + "sign_east_of_sanctuary", + "sign_east_of_castle", + "sign_north_of_lake", + "sign_desert_thief", + "sign_lumberjacks_house", + "sign_north_kakariko", + "witch_bring_mushroom", + "witch_brewing_the_item", + "witch_assistant_no_bottle", + "witch_assistant_no_empty_bottle", + "witch_assistant_informational", + "witch_assistant_no_bottle_buying", + "potion_shop_no_empty_bottles", + "item_get_lamp", + "item_get_boomerang", + "item_get_bow", + "item_get_shovel", + "item_get_magic_cape", + "item_get_powder", + "item_get_flippers", + "item_get_power_gloves", + "item_get_pendant_courage", + "item_get_pendant_power", + "item_get_pendant_wisdom", + "item_get_mushroom", + "item_get_book", + "item_get_moonpearl", + "item_get_compass", + "item_get_map", + "item_get_ice_rod", + "item_get_fire_rod", + "item_get_ether", + "item_get_bombos", + "item_get_quake", + "item_get_hammer", + "item_get_flute", + "item_get_cane_of_somaria", + "item_get_hookshot", + "item_get_bombs", + "item_get_bottle", + "item_get_big_key", + "item_get_titans_mitts", + "item_get_magic_mirror", + "item_get_fake_mastersword", + "post_item_get_mastersword", + "item_get_red_potion", + "item_get_green_potion", + "item_get_blue_potion", + "item_get_bug_net", + "item_get_blue_mail", + "item_get_red_mail", + "item_get_temperedsword", + "item_get_mirror_shield", + "item_get_cane_of_byrna", + "missing_big_key", + "missing_magic", + "item_get_pegasus_boots", + "talking_tree_info_start", + "talking_tree_info_1", + "talking_tree_info_2", + "talking_tree_info_3", + "talking_tree_info_4", + "talking_tree_other", + "item_get_pendant_power_alt", + "item_get_pendant_wisdom_alt", + "game_shooting_choice", + "game_shooting_yes", + "game_shooting_no", + "game_shooting_continue", + "pond_of_wishing", + "pond_item_select", + "pond_item_test", + "pond_will_upgrade", + "pond_item_test_no", + "pond_item_test_no_no", + "pond_item_boomerang", + "pond_item_shield", + "pond_item_silvers", + "pond_item_bottle_filled", + "pond_item_sword", + "pond_of_wishing_happiness", + "pond_of_wishing_choice", + "pond_of_wishing_bombs", + "pond_of_wishing_arrows", + "pond_of_wishing_full_upgrades", + "mountain_old_man_first", + "mountain_old_man_deadend", + "mountain_old_man_turn_right", + "mountain_old_man_lost_and_alone", + "mountain_old_man_drop_off", + "mountain_old_man_in_his_cave_pre_agahnim", + "mountain_old_man_in_his_cave", + "mountain_old_man_in_his_cave_post_agahnim", + "tavern_old_man_awake", + "tavern_old_man_unactivated_flute", + "tavern_old_man_know_tree_unactivated_flute", + "tavern_old_man_have_flute", + "chicken_hut_lady", + "running_man", + "game_race_sign", + "sign_bumper_cave", + "sign_catfish", + "sign_north_village_of_outcasts", + "sign_south_of_bumper_cave", + "sign_east_of_pyramid", + "sign_east_of_bomb_shop", + "sign_east_of_mire", + "sign_village_of_outcasts", + "sign_before_wishing_pond", + "sign_before_catfish_area", + "castle_wall_guard", + "gate_guard", + "telepathic_tile_eastern_palace", + "telepathic_tile_tower_of_hera_floor_4", + "hylian_text_1", + "mastersword_pedestal_translated", + "telepathic_tile_spectacle_rock", + "telepathic_tile_swamp_entrance", + "telepathic_tile_thieves_town_upstairs", + "telepathic_tile_misery_mire", + "hylian_text_2", + "desert_entry_translated", + "telepathic_tile_under_ganon", + "telepathic_tile_palace_of_darkness", + "telepathic_tile_desert_bonk_torch_room", + "telepathic_tile_castle_tower", + "telepathic_tile_ice_large_room", + "telepathic_tile_turtle_rock", + "telepathic_tile_ice_entrance", + "telepathic_tile_ice_stalfos_knights_room", + "telepathic_tile_tower_of_hera_entrance", + "houlihan_room", + "caught_a_bee", + "caught_a_fairy", + "no_empty_bottles", + "game_race_boy_time", + "game_race_girl", + "game_race_boy_success", + "game_race_boy_failure", + "game_race_boy_already_won", + "game_race_boy_sneaky", + "bottle_vendor_choice", + "bottle_vendor_get", + "bottle_vendor_no", + "bottle_vendor_already_collected", + "bottle_vendor_bee", + "bottle_vendor_fish", + "hobo_item_get_bottle", + "blacksmiths_what_you_want", + "blacksmiths_paywall", + "blacksmiths_extra_okay", + "blacksmiths_tempered_already", + "blacksmiths_temper_no", + "blacksmiths_bogart_sword", + "blacksmiths_get_sword", + "blacksmiths_shop_before_saving", + "blacksmiths_shop_saving", + "blacksmiths_collect_frog", + "blacksmiths_still_working", + "blacksmiths_saving_bows", + "blacksmiths_hammer_anvil", + "dark_flute_boy_storytime", + "dark_flute_boy_get_shovel", + "dark_flute_boy_no_get_shovel", + "dark_flute_boy_flute_not_found", + "dark_flute_boy_after_shovel_get", + "shop_fortune_teller_lw_hint_0", + "shop_fortune_teller_lw_hint_1", + "shop_fortune_teller_lw_hint_2", + "shop_fortune_teller_lw_hint_3", + "shop_fortune_teller_lw_hint_4", + "shop_fortune_teller_lw_hint_5", + "shop_fortune_teller_lw_hint_6", + "shop_fortune_teller_lw_hint_7", + "shop_fortune_teller_lw_no_rupees", + "shop_fortune_teller_lw", + "shop_fortune_teller_lw_post_hint", + "shop_fortune_teller_lw_no", + "shop_fortune_teller_lw_hint_8", + "shop_fortune_teller_lw_hint_9", + "shop_fortune_teller_lw_hint_10", + "shop_fortune_teller_lw_hint_11", + "shop_fortune_teller_lw_hint_12", + "shop_fortune_teller_lw_hint_13", + "shop_fortune_teller_lw_hint_14", + "shop_fortune_teller_lw_hint_15", + "dark_sanctuary", + "dark_sanctuary_hint_0", + "dark_sanctuary_no", + "dark_sanctuary_hint_1", + "dark_sanctuary_yes", + "dark_sanctuary_hint_2", + "sick_kid_no_bottle", + "sick_kid_trade", + "sick_kid_post_trade", + "desert_thief_sitting", + "desert_thief_following", + "desert_thief_question", + "desert_thief_question_yes", + "desert_thief_after_item_get", + "desert_thief_reassure", + "hylian_text_3", + "tablet_ether_book", + "tablet_bombos_book", + "magic_bat_wake", + "magic_bat_give_half_magic", + "intro_main", + "intro_throne_room", + "intro_zelda_cell", + "intro_agahnim", + "pickup_purple_chest", + "bomb_shop", + "bomb_shop_big_bomb", + "bomb_shop_big_bomb_buy", + "item_get_big_bomb", + "kiki_second_extortion", + "kiki_second_extortion_no", + "kiki_second_extortion_yes", + "kiki_first_extortion", + "kiki_first_extortion_yes", + "kiki_first_extortion_no", + "kiki_leaving_screen", + "blind_in_the_cell", + "blind_by_the_light", + "blind_not_that_way", + "aginah_l1sword_no_book", + "aginah_l1sword_with_pendants", + "aginah", + "aginah_need_better_sword", + "aginah_have_better_sword", + "catfish", + "catfish_after_item", + "lumberjack_right", + "lumberjack_left", + "lumberjack_left_post_agahnim", + "fighting_brothers_right", + "fighting_brothers_right_opened", + "fighting_brothers_left", + "maiden_crystal_1", + "maiden_crystal_2", + "maiden_crystal_3", + "maiden_crystal_4", + "maiden_crystal_5", + "maiden_crystal_6", + "maiden_crystal_7", + "maiden_ending", + "maiden_confirm_understood", + "barrier_breaking", + "maiden_crystal_7_again", + "agahnim_zelda_teleport", + "agahnim_magic_running_away", + "agahnim_hide_and_seek_found", + "agahnim_defeated", + "agahnim_final_meeting", + "zora_meeting", + "zora_tells_cost", + "zora_get_flippers", + "zora_no_cash", + "zora_no_buy_item", + "kakariko_saharalasa_grandson", + "kakariko_saharalasa_grandson_next", + "dark_palace_tree_dude", + "fairy_wishing_ponds", + "fairy_wishing_ponds_no", + "pond_of_wishing_no", + "pond_of_wishing_return_item", + "pond_of_wishing_throw", + "pond_pre_item_silvers", + "pond_of_wishing_great_luck", + "pond_of_wishing_good_luck", + "pond_of_wishing_meh_luck", + "pond_of_wishing_bad_luck", + "pond_of_wishing_fortune", + "item_get_14_heart", + "item_get_24_heart", + "item_get_34_heart", + "item_get_whole_heart", + "item_get_sanc_heart", + "fairy_fountain_refill", + "death_mountain_bullied_no_pearl", + "death_mountain_bullied_with_pearl", + "death_mountain_bully_no_pearl", + "death_mountain_bully_with_pearl", + "shop_darkworld_enter", + "game_chest_village_of_outcasts", + "game_chest_no_cash", + "game_chest_not_played", + "game_chest_played", + "game_chest_village_of_outcasts_play", + "shop_first_time", + "shop_already_have", + "shop_buy_shield", + "shop_buy_red_potion", + "shop_buy_arrows", + "shop_buy_bombs", + "shop_buy_bee", + "shop_buy_heart", + "shop_first_no_bottle_buy", + "shop_buy_no_space", + "ganon_fall_in", + "ganon_phase_3", + "lost_woods_thief", + "blinds_hut_dude", + "end_triforce", + "toppi_fallen", + "kakariko_tavern_fisherman", + "thief_money", + "thief_desert_rupee_cave", + "thief_ice_rupee_cave", + "telepathic_tile_south_east_darkworld_cave", + "cukeman", + "cukeman_2", + "potion_shop_no_cash", + "kakariko_powdered_chicken", + "game_chest_south_of_kakariko", + "game_chest_play_yes", + "game_chest_play_no", + "game_chest_lost_woods", + "kakariko_flophouse_man_no_flippers", + "kakariko_flophouse_man", + "menu_start_2", + "menu_start_3", + "menu_pause", + "game_digging_choice", + "game_digging_start", + "game_digging_no_cash", + "game_digging_end_time", + "game_digging_come_back_later", + "game_digging_no_follower", + "menu_start_4", + "ganon_fall_in_alt", + "ganon_phase_3_alt", + "sign_east_death_mountain_bridge", + "fish_money", + "sign_ganons_tower", + "sign_ganon", + "ganon_phase_3_no_bow", + "ganon_phase_3_no_silvers_alt", + "ganon_phase_3_no_silvers", + "ganon_phase_3_silvers", + "murahdahla", + ] + def __init__(self): self._text = OrderedDict() self.setDefaultText() diff --git a/worlds/generic/__init__.py b/worlds/generic/__init__.py index b88295b43237..29f808b20272 100644 --- a/worlds/generic/__init__.py +++ b/worlds/generic/__init__.py @@ -68,9 +68,3 @@ def failed(self, warning: str, exception=Exception): raise exception(warning) else: self.warn(warning) - - -class PlandoConnection(NamedTuple): - entrance: str - exit: str - direction: str # entrance, exit or both diff --git a/worlds/kdl3/Options.py b/worlds/kdl3/Options.py index 336bd33bc583..e0a4f12f15dc 100644 --- a/worlds/kdl3/Options.py +++ b/worlds/kdl3/Options.py @@ -2,10 +2,14 @@ from dataclasses import dataclass from Options import DeathLink, Choice, Toggle, OptionDict, Range, PlandoBosses, DefaultOnToggle, \ - PerGameCommonOptions + PerGameCommonOptions, PlandoConnections from .Names import LocationName +class KDL3PlandoConnections(PlandoConnections): + entrances = exits = {f"{i} {j}" for i in LocationName.level_names for j in range(1, 7)} + + class Goal(Choice): """ Zero: collect the Heart Stars, and defeat Zero in the Hyper Zone. @@ -400,6 +404,7 @@ class Gifting(Toggle): @dataclass class KDL3Options(PerGameCommonOptions): + plando_connections: KDL3PlandoConnections death_link: DeathLink game_language: GameLanguage goal: Goal diff --git a/worlds/kdl3/Regions.py b/worlds/kdl3/Regions.py index ac27d8bbf517..407dcf9680f4 100644 --- a/worlds/kdl3/Regions.py +++ b/worlds/kdl3/Regions.py @@ -129,8 +129,8 @@ def generate_valid_levels(world: "KDL3World", enforce_world: bool, enforce_patte } possible_stages = [default_levels[level][stage] for level in default_levels for stage in range(6)] - if world.multiworld.plando_connections[world.player]: - for connection in world.multiworld.plando_connections[world.player]: + if world.options.plando_connections: + for connection in world.options.plando_connections: try: entrance_world, entrance_stage = connection.entrance.rsplit(" ", 1) stage_world, stage_stage = connection.exit.rsplit(" ", 1) diff --git a/worlds/kdl3/test/__init__.py b/worlds/kdl3/test/__init__.py index 11a17e63b7fa..4d3f4d70faae 100644 --- a/worlds/kdl3/test/__init__.py +++ b/worlds/kdl3/test/__init__.py @@ -2,7 +2,7 @@ from argparse import Namespace from BaseClasses import MultiWorld, PlandoOptions, CollectionState -from test.TestBase import WorldTestBase +from test.bases import WorldTestBase from test.general import gen_steps from worlds import AutoWorld from worlds.AutoWorld import call_all @@ -32,6 +32,5 @@ def world_setup(self, seed: typing.Optional[int] = None) -> None: }) self.multiworld.set_options(args) self.multiworld.plando_options = PlandoOptions.connections - self.multiworld.plando_connections = self.options["plando_connections"] if "plando_connections" in self.options.keys() else [] for step in gen_steps: call_all(self.multiworld, step) diff --git a/worlds/kdl3/test/test_locations.py b/worlds/kdl3/test/test_locations.py index 433b4534d1e5..bde9abc409ac 100644 --- a/worlds/kdl3/test/test_locations.py +++ b/worlds/kdl3/test/test_locations.py @@ -1,5 +1,5 @@ from . import KDL3TestBase -from worlds.generic import PlandoConnection +from Options import PlandoConnection from ..Names import LocationName import typing @@ -49,12 +49,10 @@ class TestShiro(KDL3TestBase): options = { "open_world": False, "plando_connections": [ - [], - [ PlandoConnection("Grass Land 1", "Iceberg 5", "both"), PlandoConnection("Grass Land 2", "Ripple Field 5", "both"), PlandoConnection("Grass Land 3", "Grass Land 1", "both") - ]], + ], "stage_shuffle": "shuffled", "plando_options": "connections" } diff --git a/worlds/messenger/options.py b/worlds/messenger/options.py index 0d8fcf4da55f..73adf4ebdf0a 100644 --- a/worlds/messenger/options.py +++ b/worlds/messenger/options.py @@ -3,8 +3,9 @@ from schema import And, Optional, Or, Schema -from Options import Accessibility, Choice, DeathLinkMixin, DefaultOnToggle, OptionDict, PerGameCommonOptions, Range, \ - StartInventoryPool, Toggle +from Options import Accessibility, Choice, DeathLinkMixin, DefaultOnToggle, OptionDict, PerGameCommonOptions, \ + PlandoConnections, Range, StartInventoryPool, Toggle, Visibility +from worlds.messenger.portals import CHECKPOINTS, PORTALS, SHOP_POINTS class MessengerAccessibility(Accessibility): @@ -13,6 +14,36 @@ class MessengerAccessibility(Accessibility): __doc__ = Accessibility.__doc__.replace(f"default {Accessibility.default}", f"default {default}") +class PortalPlando(PlandoConnections): + """ + Plando connections to be used with portal shuffle. Direction is ignored. + List of valid connections can be found here: https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/messenger/portals.py#L12. + The entering Portal should *not* have "Portal" appended. + For the exits, those in checkpoints and shops should just be the name of the spot, while portals should have " Portal" at the end. + Example: + - entrance: Riviere Turquoise + exit: Wingsuit + - entrance: Sunken Shrine + exit: Sunny Day + - entrance: Searing Crags + exit: Glacial Peak Portal + """ + portals = [f"{portal} Portal" for portal in PORTALS] + shop_points = [point for points in SHOP_POINTS.values() for point in points] + checkpoints = [point for points in CHECKPOINTS.values() for point in points] + portal_entrances = PORTALS + portal_exits = portals + shop_points + checkpoints + entrances = portal_entrances + exits = portal_exits + + +# for back compatibility. To later be replaced with transition plando +class HiddenPortalPlando(PortalPlando): + visibility = Visibility.none + entrances = PortalPlando.entrances + exits = PortalPlando.exits + + class Logic(Choice): """ The level of logic to use when determining what locations in your world are accessible. @@ -205,3 +236,5 @@ class MessengerOptions(DeathLinkMixin, PerGameCommonOptions): traps: Traps shop_price: ShopPrices shop_price_plan: PlannedShopPrices + portal_plando: PortalPlando + plando_connections: HiddenPortalPlando diff --git a/worlds/messenger/portals.py b/worlds/messenger/portals.py index f5603736c3a7..1da210cb23ff 100644 --- a/worlds/messenger/portals.py +++ b/worlds/messenger/portals.py @@ -2,8 +2,7 @@ from typing import List, TYPE_CHECKING from BaseClasses import CollectionState, PlandoOptions -from worlds.generic import PlandoConnection -from .options import ShufflePortals +from Options import PlandoConnection if TYPE_CHECKING: from . import MessengerWorld @@ -207,6 +206,8 @@ def shuffle_portals(world: "MessengerWorld") -> None: """shuffles the output of the portals from the main hub""" + from .options import ShufflePortals + def create_mapping(in_portal: str, warp: str) -> str: """assigns the chosen output to the input""" parent = out_to_parent[warp] @@ -247,7 +248,9 @@ def handle_planned_portals(plando_connections: List[PlandoConnection]) -> None: available_portals = [val for zone in shop_points.values() for val in zone] world.random.shuffle(available_portals) - plando = world.multiworld.plando_connections[world.player] + plando = world.options.portal_plando.value + if not plando: + plando = world.options.plando_connections.value if plando and world.multiworld.plando_options & PlandoOptions.connections: handle_planned_portals(plando) diff --git a/worlds/minecraft/Options.py b/worlds/minecraft/Options.py index cdb5bf303f47..9407097b4638 100644 --- a/worlds/minecraft/Options.py +++ b/worlds/minecraft/Options.py @@ -1,5 +1,6 @@ import typing -from Options import Choice, Option, Toggle, DefaultOnToggle, Range, OptionList, DeathLink +from Options import Choice, Option, Toggle, DefaultOnToggle, Range, OptionList, DeathLink, PlandoConnections +from .Constants import region_info class AdvancementGoal(Range): @@ -97,7 +98,19 @@ class StartingItems(OptionList): display_name = "Starting Items" +class MCPlandoConnections(PlandoConnections): + entrances = set(connection[0] for connection in region_info["default_connections"]) + exits = set(connection[1] for connection in region_info["default_connections"]) + + @classmethod + def can_connect(cls, entrance, exit): + if exit in region_info["illegal_connections"] and entrance in region_info["illegal_connections"][exit]: + return False + return True + + minecraft_options: typing.Dict[str, type(Option)] = { + "plando_connections": MCPlandoConnections, "advancement_goal": AdvancementGoal, "egg_shards_required": EggShardsRequired, "egg_shards_available": EggShardsAvailable, diff --git a/worlds/oot/Options.py b/worlds/oot/Options.py index 2543cdc715c7..daf072adb59c 100644 --- a/worlds/oot/Options.py +++ b/worlds/oot/Options.py @@ -1,6 +1,7 @@ import typing import random -from Options import Option, DefaultOnToggle, Toggle, Range, OptionList, OptionSet, DeathLink +from Options import Option, DefaultOnToggle, Toggle, Range, OptionList, OptionSet, DeathLink, PlandoConnections +from .EntranceShuffle import entrance_shuffle_table from .LogicTricks import normalized_name_tricks from .ColorSFXOptions import * @@ -29,6 +30,11 @@ def from_any(cls, data: typing.Any) -> Range: raise RuntimeError(f"All options specified in \"{cls.display_name}\" are weighted as zero.") +class OoTPlandoConnections(PlandoConnections): + entrances = set([connection[1][0] for connection in entrance_shuffle_table]) + exits = set([connection[2][0] for connection in entrance_shuffle_table if len(connection) > 2]) + + class Logic(Choice): """Set the logic used for the generator. Glitchless: Normal gameplay. Can enable more difficult logical paths using the Logic Tricks option. @@ -1277,6 +1283,7 @@ class LogicTricks(OptionList): # All options assembled into a single dict oot_options: typing.Dict[str, type(Option)] = { + "plando_connections": OoTPlandoConnections, "logic_rules": Logic, "logic_no_night_tokens_without_suns_song": NightTokens, **open_options, diff --git a/worlds/oot/__init__.py b/worlds/oot/__init__.py index 9346ac55103a..34b3935fec4e 100644 --- a/worlds/oot/__init__.py +++ b/worlds/oot/__init__.py @@ -32,7 +32,7 @@ from Utils import get_options from BaseClasses import MultiWorld, CollectionState, Tutorial, LocationProgressType -from Options import Range, Toggle, VerifyKeys, Accessibility +from Options import Range, Toggle, VerifyKeys, Accessibility, PlandoConnections from Fill import fill_restrictive, fast_fill, FillError from worlds.generic.Rules import exclusion_rules, add_item_rule from ..AutoWorld import World, AutoLogicRegister, WebWorld @@ -201,6 +201,8 @@ def generate_early(self): option_value = bool(result) elif isinstance(result, VerifyKeys): option_value = result.value + elif isinstance(result, PlandoConnections): + option_value = result.value else: option_value = result.current_key setattr(self, option_name, option_value) diff --git a/worlds/tunic/__init__.py b/worlds/tunic/__init__.py index cff8c39c9fea..9ef5800955aa 100644 --- a/worlds/tunic/__init__.py +++ b/worlds/tunic/__init__.py @@ -10,7 +10,7 @@ from .er_data import portal_mapping from .options import TunicOptions, EntranceRando, tunic_option_groups, tunic_option_presets from worlds.AutoWorld import WebWorld, World -from worlds.generic import PlandoConnection +from Options import PlandoConnection from decimal import Decimal, ROUND_HALF_UP @@ -70,17 +70,17 @@ class TunicWorld(World): seed_groups: Dict[str, SeedGroup] = {} def generate_early(self) -> None: - if self.multiworld.plando_connections[self.player]: - for index, cxn in enumerate(self.multiworld.plando_connections[self.player]): + if self.options.plando_connections: + for index, cxn in enumerate(self.options.plando_connections): # making shops second to simplify other things later if cxn.entrance.startswith("Shop"): replacement = PlandoConnection(cxn.exit, "Shop Portal", "both") - self.multiworld.plando_connections[self.player].remove(cxn) - self.multiworld.plando_connections[self.player].insert(index, replacement) + self.options.plando_connections.value.remove(cxn) + self.options.plando_connections.value.insert(index, replacement) elif cxn.exit.startswith("Shop"): replacement = PlandoConnection(cxn.entrance, "Shop Portal", "both") - self.multiworld.plando_connections[self.player].remove(cxn) - self.multiworld.plando_connections[self.player].insert(index, replacement) + self.options.plando_connections.value.remove(cxn) + self.options.plando_connections.value.insert(index, replacement) # Universal tracker stuff, shouldn't do anything in standard gen if hasattr(self.multiworld, "re_gen_passthrough"): diff --git a/worlds/tunic/er_scripts.py b/worlds/tunic/er_scripts.py index 3abdfecce233..7e022c9f3a0d 100644 --- a/worlds/tunic/er_scripts.py +++ b/worlds/tunic/er_scripts.py @@ -3,8 +3,8 @@ from .locations import location_table from .er_data import Portal, tunic_er_regions, portal_mapping, traversal_requirements, DeadEnd from .er_rules import set_er_region_rules +from Options import PlandoConnection from .options import EntranceRando -from worlds.generic import PlandoConnection from random import Random from copy import deepcopy @@ -194,7 +194,7 @@ def pair_portals(world: "TunicWorld") -> Dict[Portal, Portal]: connected_regions = update_reachable_regions(connected_regions, traversal_reqs, has_laurels, logic_rules) if world.options.entrance_rando.value in EntranceRando.options: - plando_connections = world.multiworld.plando_connections[world.player] + plando_connections = world.options.plando_connections.value else: plando_connections = world.seed_groups[world.options.entrance_rando.value]["plando"] diff --git a/worlds/tunic/options.py b/worlds/tunic/options.py index a45ee71b0557..b3b6b3b96fb0 100644 --- a/worlds/tunic/options.py +++ b/worlds/tunic/options.py @@ -1,7 +1,8 @@ from dataclasses import dataclass from typing import Dict, Any -from Options import (DefaultOnToggle, Toggle, StartInventoryPool, Choice, Range, TextChoice, PerGameCommonOptions, - OptionGroup) +from Options import (DefaultOnToggle, Toggle, StartInventoryPool, Choice, Range, TextChoice, PlandoConnections, + PerGameCommonOptions, OptionGroup) +from .er_data import portal_mapping class SwordProgression(DefaultOnToggle): @@ -170,6 +171,13 @@ class ShuffleLadders(Toggle): """ internal_name = "shuffle_ladders" display_name = "Shuffle Ladders" + + +class TUNICPlandoConnections(PlandoConnections): + entrances = {*(portal.name for portal in portal_mapping), "Shop", "Shop Portal"} + exits = {*(portal.name for portal in portal_mapping), "Shop", "Shop Portal"} + + duplicate_exits = True @dataclass @@ -190,7 +198,8 @@ class TunicOptions(PerGameCommonOptions): lanternless: Lanternless maskless: Maskless laurels_location: LaurelsLocation - + plando_connections: TUNICPlandoConnections + tunic_option_groups = [ OptionGroup("Logic Options", [ From 97c9c5310bb89a04c862de7689925802397942b0 Mon Sep 17 00:00:00 2001 From: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> Date: Sat, 1 Jun 2024 07:35:33 -0400 Subject: [PATCH 48/64] PKMN R/B: Fixing Key Items Only + Removed Exp. All (#3420) --- worlds/pokemon_rb/regions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worlds/pokemon_rb/regions.py b/worlds/pokemon_rb/regions.py index 4932f5793583..a9206fe66753 100644 --- a/worlds/pokemon_rb/regions.py +++ b/worlds/pokemon_rb/regions.py @@ -1560,7 +1560,7 @@ def create_regions(self): <= self.multiworld.trap_percentage[self.player].value and combined_traps != 0): item = self.create_item(self.select_trap()) - if self.multiworld.key_items_only[self.player] and (not location.event) and (not item.advancement): + if self.multiworld.key_items_only[self.player] and (not location.event) and (not item.advancement) and location.original_item != "Exp. All": continue if item.name in start_inventory and start_inventory[item.name] > 0 and \ From 1e205f9d738fbcb552c29722c879afd1b9f757f2 Mon Sep 17 00:00:00 2001 From: Dinopony Date: Sat, 1 Jun 2024 13:39:57 +0200 Subject: [PATCH 49/64] Landstalker: Fixed rare generation issues (#3353) Co-authored-by: Fabian Dill --- worlds/landstalker/Hints.py | 5 ++++- worlds/landstalker/Locations.py | 15 ++++++++++++++- worlds/landstalker/Regions.py | 2 +- worlds/landstalker/Rules.py | 2 +- worlds/landstalker/__init__.py | 3 +++ 5 files changed, 23 insertions(+), 4 deletions(-) diff --git a/worlds/landstalker/Hints.py b/worlds/landstalker/Hints.py index 93274f1d68bb..5309e85032ea 100644 --- a/worlds/landstalker/Hints.py +++ b/worlds/landstalker/Hints.py @@ -30,6 +30,9 @@ def generate_lithograph_hint(world: "LandstalkerWorld"): jewel_items = world.jewel_items for item in jewel_items: + if item.location is None: + continue + # Jewel hints are composed of 4 'words' shuffled randomly: # - the name of the player whose world contains said jewel (if not ours) # - the color of the jewel (if relevant) @@ -61,7 +64,7 @@ def generate_random_hints(world: "LandstalkerWorld"): excluded_items = ["Life Stock", "EkeEke"] progression_items = [item for item in multiworld.itempool if item.advancement and - item.name not in excluded_items] + item.name not in excluded_items and item.location is not None] local_own_progression_items = [item for item in progression_items if item.player == this_player and item.location.player == this_player] diff --git a/worlds/landstalker/Locations.py b/worlds/landstalker/Locations.py index 5e42fbecda72..b0148269eab3 100644 --- a/worlds/landstalker/Locations.py +++ b/worlds/landstalker/Locations.py @@ -1,8 +1,9 @@ from typing import Dict, Optional -from BaseClasses import Location +from BaseClasses import Location, ItemClassification, Item from .Regions import LandstalkerRegion from .data.item_source import ITEM_SOURCES_JSON +from .data.world_path import WORLD_PATHS_JSON BASE_LOCATION_ID = 4000 BASE_GROUND_LOCATION_ID = BASE_LOCATION_ID + 256 @@ -28,6 +29,18 @@ def create_locations(player: int, regions_table: Dict[str, LandstalkerRegion], n new_location = LandstalkerLocation(player, data["name"], name_to_id_table[data["name"]], region, data["type"]) region.locations.append(new_location) + # Create fake event locations that will be used to determine if some key regions has been visited + regions_with_entrance_checks = [] + for data in WORLD_PATHS_JSON: + if "requiredNodes" in data: + regions_with_entrance_checks.extend([region_id for region_id in data["requiredNodes"]]) + regions_with_entrance_checks = list(set(regions_with_entrance_checks)) + for region_id in regions_with_entrance_checks: + region = regions_table[region_id] + location = LandstalkerLocation(player, 'event_visited_' + region_id, None, region, "event") + location.place_locked_item(Item("event_visited_" + region_id, ItemClassification.progression, None, player)) + region.locations.append(location) + # Create a specific end location that will contain a fake win-condition item end_location = LandstalkerLocation(player, "End", None, regions_table["end"], "reward") regions_table["end"].locations.append(end_location) diff --git a/worlds/landstalker/Regions.py b/worlds/landstalker/Regions.py index 21704194f157..27e5e2e993d0 100644 --- a/worlds/landstalker/Regions.py +++ b/worlds/landstalker/Regions.py @@ -37,7 +37,7 @@ def create_regions(world: "LandstalkerWorld"): for code, region_data in WORLD_NODES_JSON.items(): random_hint_name = None if "hints" in region_data: - random_hint_name = multiworld.random.choice(region_data["hints"]) + random_hint_name = world.random.choice(region_data["hints"]) region = LandstalkerRegion(code, region_data["name"], player, multiworld, random_hint_name) regions_table[code] = region multiworld.regions.append(region) diff --git a/worlds/landstalker/Rules.py b/worlds/landstalker/Rules.py index 51357c9480b0..94171944d7b2 100644 --- a/worlds/landstalker/Rules.py +++ b/worlds/landstalker/Rules.py @@ -10,7 +10,7 @@ def _landstalker_has_visited_regions(state: CollectionState, player: int, regions): - return all([state.can_reach(region, None, player) for region in regions]) + return all(state.has("event_visited_" + region.code, player) for region in regions) def _landstalker_has_health(state: CollectionState, player: int, health): diff --git a/worlds/landstalker/__init__.py b/worlds/landstalker/__init__.py index baa1deb620a4..2b3dc41239c3 100644 --- a/worlds/landstalker/__init__.py +++ b/worlds/landstalker/__init__.py @@ -204,6 +204,9 @@ def set_rules(self): for location in self.multiworld.get_locations(self.player): if location.parent_region.name in excluded_regions: location.progress_type = LocationProgressType.EXCLUDED + # We need to make that event non-progression since it would crash generation in reach_kazalt goal + if location.item is not None and location.item.name == "event_visited_king_nole_labyrinth_raft_entrance": + location.item.classification = ItemClassification.filler def get_starting_health(self): spawn_id = self.options.spawn_region.current_key From 8dbc8d2d41c743b9425809065f90f2d7935fe626 Mon Sep 17 00:00:00 2001 From: Nicholas Saylor <79181893+nicholassaylor@users.noreply.github.com> Date: Sat, 1 Jun 2024 07:42:02 -0400 Subject: [PATCH 50/64] Installer: Prevent ALTTP Sprite Download from being Interrupted (#3293) --- inno_setup.iss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/inno_setup.iss b/inno_setup.iss index 4b37279e8de0..b016f224dfcf 100644 --- a/inno_setup.iss +++ b/inno_setup.iss @@ -75,7 +75,7 @@ Name: "{commondesktop}\{#MyAppName} Launcher"; Filename: "{app}\ArchipelagoLaunc [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..."; Flags: nowait; Components: lttp_sprites +Filename: "{app}\ArchipelagoLttPAdjuster"; Parameters: "--update_sprites"; StatusMsg: "Updating Sprite Library..."; 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 From 3cb545245528e46e9c0b72ade32839865efb3780 Mon Sep 17 00:00:00 2001 From: Ishigh1 Date: Sat, 1 Jun 2024 14:32:41 +0200 Subject: [PATCH 51/64] Core: Fix auto-fill in the text client when clicking on a hint suggestion (#3267) --- MultiServer.py | 24 +------------ Utils.py | 35 +++++++++++++++++++ kvui.py | 16 +++------ .../general/test_client_server_interaction.py | 23 ++++++++++++ 4 files changed, 64 insertions(+), 34 deletions(-) create mode 100644 test/general/test_client_server_interaction.py diff --git a/MultiServer.py b/MultiServer.py index 2c08b0b4eb08..22375da2b3c5 100644 --- a/MultiServer.py +++ b/MultiServer.py @@ -37,7 +37,7 @@ import NetUtils import Utils -from Utils import version_tuple, restricted_loads, Version, async_start +from Utils import version_tuple, restricted_loads, Version, async_start, get_intended_text from NetUtils import Endpoint, ClientStatus, NetworkItem, decode, encode, NetworkPlayer, Permission, NetworkSlot, \ SlotType, LocationStore @@ -1095,28 +1095,6 @@ def json_format_send_event(net_item: NetworkItem, receiving_player: int): "item": net_item} -def get_intended_text(input_text: str, possible_answers) -> typing.Tuple[str, bool, str]: - picks = Utils.get_fuzzy_results(input_text, possible_answers, limit=2) - if len(picks) > 1: - dif = picks[0][1] - picks[1][1] - if picks[0][1] == 100: - return picks[0][0], True, "Perfect Match" - elif picks[0][1] < 75: - return picks[0][0], False, f"Didn't find something that closely matches '{input_text}', " \ - f"did you mean '{picks[0][0]}'? ({picks[0][1]}% sure)" - elif dif > 5: - return picks[0][0], True, "Close Match" - else: - return picks[0][0], False, f"Too many close matches for '{input_text}', " \ - f"did you mean '{picks[0][0]}'? ({picks[0][1]}% sure)" - else: - if picks[0][1] > 90: - return picks[0][0], True, "Only Option Match" - else: - return picks[0][0], False, f"Didn't find something that closely matches '{input_text}', " \ - f"did you mean '{picks[0][0]}'? ({picks[0][1]}% sure)" - - class CommandMeta(type): def __new__(cls, name, bases, attrs): commands = attrs["commands"] = {} diff --git a/Utils.py b/Utils.py index 9f6837215c5e..eea81a2d3201 100644 --- a/Utils.py +++ b/Utils.py @@ -622,6 +622,41 @@ def get_fuzzy_ratio(word1: str, word2: str) -> float: ) +def get_intended_text(input_text: str, possible_answers) -> typing.Tuple[str, bool, str]: + picks = get_fuzzy_results(input_text, possible_answers, limit=2) + if len(picks) > 1: + dif = picks[0][1] - picks[1][1] + if picks[0][1] == 100: + return picks[0][0], True, "Perfect Match" + elif picks[0][1] < 75: + return picks[0][0], False, f"Didn't find something that closely matches '{input_text}', " \ + f"did you mean '{picks[0][0]}'? ({picks[0][1]}% sure)" + elif dif > 5: + return picks[0][0], True, "Close Match" + else: + return picks[0][0], False, f"Too many close matches for '{input_text}', " \ + f"did you mean '{picks[0][0]}'? ({picks[0][1]}% sure)" + else: + if picks[0][1] > 90: + return picks[0][0], True, "Only Option Match" + else: + return picks[0][0], False, f"Didn't find something that closely matches '{input_text}', " \ + f"did you mean '{picks[0][0]}'? ({picks[0][1]}% sure)" + + +def get_input_text_from_response(text: str, command: str) -> typing.Optional[str]: + if "did you mean " in text: + for question in ("Didn't find something that closely matches", + "Too many close matches"): + if text.startswith(question): + name = get_text_between(text, "did you mean '", + "'? (") + return f"!{command} {name}" + elif text.startswith("Missing: "): + return text.replace("Missing: ", "!hint_location ") + return None + + def open_filename(title: str, filetypes: typing.Iterable[typing.Tuple[str, typing.Iterable[str]]], suggest: str = "") \ -> typing.Optional[str]: logging.info(f"Opening file input dialog for {title}.") diff --git a/kvui.py b/kvui.py index 98aa9516b266..e9e495aef366 100644 --- a/kvui.py +++ b/kvui.py @@ -64,7 +64,7 @@ fade_in_animation = Animation(opacity=0, duration=0) + Animation(opacity=1, duration=0.25) from NetUtils import JSONtoTextParser, JSONMessagePart, SlotType -from Utils import async_start +from Utils import async_start, get_input_text_from_response if typing.TYPE_CHECKING: import CommonClient @@ -285,16 +285,10 @@ def on_touch_down(self, touch): temp = MarkupLabel(text=self.text).markup text = "".join(part for part in temp if not part.startswith(("[color", "[/color]", "[ref=", "[/ref]"))) cmdinput = App.get_running_app().textinput - if not cmdinput.text and " did you mean " in text: - for question in ("Didn't find something that closely matches, did you mean ", - "Too many close matches, did you mean "): - if text.startswith(question): - name = Utils.get_text_between(text, question, - "? (") - cmdinput.text = f"!{App.get_running_app().last_autofillable_command} {name}" - break - elif not cmdinput.text and text.startswith("Missing: "): - cmdinput.text = text.replace("Missing: ", "!hint_location ") + if not cmdinput.text: + input_text = get_input_text_from_response(text, App.get_running_app().last_autofillable_command) + if input_text is not None: + cmdinput.text = input_text Clipboard.copy(text.replace("&", "&").replace("&bl;", "[").replace("&br;", "]")) return self.parent.select_with_touch(self.index, touch) diff --git a/test/general/test_client_server_interaction.py b/test/general/test_client_server_interaction.py new file mode 100644 index 000000000000..17de91517409 --- /dev/null +++ b/test/general/test_client_server_interaction.py @@ -0,0 +1,23 @@ +import unittest + +from Utils import get_intended_text, get_input_text_from_response + + +class TestClient(unittest.TestCase): + def test_autofill_hint_from_fuzzy_hint(self) -> None: + tests = ( + ("item", ["item1", "item2"]), # Multiple close matches + ("itm", ["item1", "item21"]), # No close match, multiple option + ("item", ["item1"]), # No close match, single option + ("item", ["\"item\" 'item' (item)"]), # Testing different special characters + ) + + for input_text, possible_answers in tests: + item_name, usable, response = get_intended_text(input_text, possible_answers) + self.assertFalse(usable, "This test must be updated, it seems get_fuzzy_results behavior changed") + + hint_command = get_input_text_from_response(response, "hint") + self.assertIsNotNone(hint_command, + "The response to fuzzy hints is no longer recognized by the hint autofill") + self.assertEqual(hint_command, f"!hint {item_name}", + "The hint command autofilled by the response is not correct") From bbc79a5b998f1450fba90b0290745f88a7ffefc5 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Sat, 1 Jun 2024 14:38:45 +0200 Subject: [PATCH 52/64] LttP: allow Triforce Piece as start inventory item (#3292) --- worlds/alttp/Rom.py | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/worlds/alttp/Rom.py b/worlds/alttp/Rom.py index f451e88fdb2b..224de6aaf7f3 100644 --- a/worlds/alttp/Rom.py +++ b/worlds/alttp/Rom.py @@ -1269,7 +1269,8 @@ def chunk(l, n): rom.write_int32(0x18020C, 0) # starting time (in frames, sint32) # set up goals for treasure hunt - rom.write_int16(0x180163, local_world.treasure_hunt_required) + rom.write_int16(0x180163, max(0, local_world.treasure_hunt_required - + sum(1 for item in world.precollected_items[player] if item.name == "Triforce Piece"))) rom.write_bytes(0x180165, [0x0E, 0x28]) # Triforce Piece Sprite rom.write_byte(0x180194, 1) # Must turn in triforced pieces (instant win not enabled) @@ -1372,7 +1373,7 @@ def chunk(l, n): 'Golden Sword', 'Tempered Sword', 'Master Sword', 'Fighter Sword', 'Progressive Sword', 'Mirror Shield', 'Red Shield', 'Blue Shield', 'Progressive Shield', 'Red Mail', 'Blue Mail', 'Progressive Mail', - 'Magic Upgrade (1/4)', 'Magic Upgrade (1/2)'}: + 'Magic Upgrade (1/4)', 'Magic Upgrade (1/2)', 'Triforce Piece'}: continue set_table = {'Book of Mudora': (0x34E, 1), 'Hammer': (0x34B, 1), 'Bug Catching Net': (0x34D, 1), @@ -2475,6 +2476,9 @@ def hint_text(dest, ped_hint=False): tt['sahasrahla_quest_have_master_sword'] = Sahasrahla2_texts[local_random.randint(0, len(Sahasrahla2_texts) - 1)] tt['blind_by_the_light'] = Blind_texts[local_random.randint(0, len(Blind_texts) - 1)] + triforce_pieces_required = max(0, w.treasure_hunt_required - + sum(1 for item in world.precollected_items[player] if item.name == "Triforce Piece")) + if world.goal[player] in ['triforce_hunt', 'local_triforce_hunt']: tt['ganon_fall_in_alt'] = 'Why are you even here?\n You can\'t even hurt me! Get the Triforce Pieces.' tt['ganon_phase_3_alt'] = 'Seriously? Go Away, I will not Die.' @@ -2482,16 +2486,16 @@ def hint_text(dest, ped_hint=False): tt['sign_ganon'] = 'Go find the Triforce pieces with your friends... Ganon is invincible!' else: tt['sign_ganon'] = 'Go find the Triforce pieces... Ganon is invincible!' - if w.treasure_hunt_required > 1: + if triforce_pieces_required > 1: tt['murahdahla'] = "Hello @. I\nam Murahdahla, brother of\nSahasrahla and Aginah. Behold the power of\n" \ "invisibility.\n\n\n\n… … …\n\nWait! you can see me? I knew I should have\n" \ "hidden in a hollow tree. If you bring\n%d Triforce pieces out of %d, I can reassemble it." % \ - (w.treasure_hunt_required, w.treasure_hunt_total) + (triforce_pieces_required, w.treasure_hunt_total) else: tt['murahdahla'] = "Hello @. I\nam Murahdahla, brother of\nSahasrahla and Aginah. Behold the power of\n" \ "invisibility.\n\n\n\n… … …\n\nWait! you can see me? I knew I should have\n" \ "hidden in a hollow tree. If you bring\n%d Triforce piece out of %d, I can reassemble it." % \ - (w.treasure_hunt_required, w.treasure_hunt_total) + (triforce_pieces_required, w.treasure_hunt_total) elif world.goal[player] in ['pedestal']: tt['ganon_fall_in_alt'] = 'Why are you even here?\n You can\'t even hurt me! Your goal is at the pedestal.' tt['ganon_phase_3_alt'] = 'Seriously? Go Away, I will not Die.' @@ -2500,20 +2504,20 @@ def hint_text(dest, ped_hint=False): tt['ganon_fall_in'] = Ganon1_texts[local_random.randint(0, len(Ganon1_texts) - 1)] tt['ganon_fall_in_alt'] = 'You cannot defeat me until you finish your goal!' tt['ganon_phase_3_alt'] = 'Got wax in\nyour ears?\nI can not die!' - if w.treasure_hunt_required > 1: + if triforce_pieces_required > 1: if world.goal[player] == 'ganon_triforce_hunt' and world.players > 1: tt['sign_ganon'] = 'You need to find %d Triforce pieces out of %d with your friends to defeat Ganon.' % \ - (w.treasure_hunt_required, w.treasure_hunt_total) + (triforce_pieces_required, w.treasure_hunt_total) elif world.goal[player] in ['ganon_triforce_hunt', 'local_ganon_triforce_hunt']: tt['sign_ganon'] = 'You need to find %d Triforce pieces out of %d to defeat Ganon.' % \ - (w.treasure_hunt_required, w.treasure_hunt_total) + (triforce_pieces_required, w.treasure_hunt_total) else: if world.goal[player] == 'ganon_triforce_hunt' and world.players > 1: tt['sign_ganon'] = 'You need to find %d Triforce piece out of %d with your friends to defeat Ganon.' % \ - (w.treasure_hunt_required, w.treasure_hunt_total) + (triforce_pieces_required, w.treasure_hunt_total) elif world.goal[player] in ['ganon_triforce_hunt', 'local_ganon_triforce_hunt']: tt['sign_ganon'] = 'You need to find %d Triforce piece out of %d to defeat Ganon.' % \ - (w.treasure_hunt_required, w.treasure_hunt_total) + (triforce_pieces_required, w.treasure_hunt_total) tt['kakariko_tavern_fisherman'] = TavernMan_texts[local_random.randint(0, len(TavernMan_texts) - 1)] From 13bc121c27228691d6e8a90952e918d69f04425d Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Sat, 1 Jun 2024 14:43:11 +0200 Subject: [PATCH 53/64] Webhost: Sphere Tracker (#3412) --- WebHostLib/templates/hostRoom.html | 3 +- WebHostLib/templates/multispheretracker.html | 72 ++++++++++++++++++++ WebHostLib/tracker.py | 25 +++++++ 3 files changed, 99 insertions(+), 1 deletion(-) create mode 100644 WebHostLib/templates/multispheretracker.html diff --git a/WebHostLib/templates/hostRoom.html b/WebHostLib/templates/hostRoom.html index 2981c41452f0..2bbfe4ad0169 100644 --- a/WebHostLib/templates/hostRoom.html +++ b/WebHostLib/templates/hostRoom.html @@ -24,7 +24,8 @@
{% endif %} {% if room.tracker %} - This room has a Multiworld Tracker enabled. + This room has a Multiworld Tracker + and a Sphere Tracker enabled.
{% endif %} The server for this room will be paused after {{ room.timeout//60//60 }} hours of inactivity. diff --git a/WebHostLib/templates/multispheretracker.html b/WebHostLib/templates/multispheretracker.html new file mode 100644 index 000000000000..a86697498396 --- /dev/null +++ b/WebHostLib/templates/multispheretracker.html @@ -0,0 +1,72 @@ +{% extends "tablepage.html" %} +{% block head %} + {{ super() }} + Multiworld Sphere Tracker + + +{% endblock %} + +{% block body %} + {% include "header/dirtHeader.html" %} + +
+
+ + +
+ {% if tracker_data.get_spheres() %} + This tracker lists already found locations by their logical access sphere. + It ignores items that cannot be sent + and will therefore differ from the sphere numbers in the spoiler playthrough. + This tracker will automatically update itself periodically. + {% else %} + This Multiworld has no Sphere data, likely due to being too old, cannot display data. + {% endif %} +
+
+ +
+ {%- for team, players in tracker_data.get_all_players().items() %} +
+ + + + + {#- Mimicking hint table header for familiarity. #} + + + + + + + + + {%- for sphere in tracker_data.get_spheres() %} + {%- set current_sphere = loop.index %} + {%- for player, sphere_location_ids in sphere.items() %} + {%- set checked_locations = tracker_data.get_player_checked_locations(team, player) %} + {%- set finder_game = tracker_data.get_player_game(team, player) %} + {%- set player_location_data = tracker_data.get_player_locations(team, player) %} + {%- for location_id in sphere_location_ids.intersection(checked_locations) %} + + {%- set item_id, receiver, item_flags = player_location_data[location_id] %} + {%- set receiver_game = tracker_data.get_player_game(team, receiver) %} + + + + + + + + {%- endfor %} + + {%- endfor %} + {%- endfor %} + +
SphereFinderReceiverItemLocationGame
{{ current_sphere }}{{ tracker_data.get_player_name(team, player) }}{{ tracker_data.get_player_name(team, receiver) }}{{ tracker_data.item_id_to_name[receiver_game][item_id] }}{{ tracker_data.location_id_to_name[finder_game][location_id] }}{{ finder_game }}
+
+ + {%- endfor -%} +
+
+{% endblock %} diff --git a/WebHostLib/tracker.py b/WebHostLib/tracker.py index fd233da131e7..71ee9c7fcafe 100644 --- a/WebHostLib/tracker.py +++ b/WebHostLib/tracker.py @@ -291,6 +291,11 @@ def get_room_videos(self) -> Dict[TeamPlayer, Tuple[str, str]]: return video_feeds + @_cache_results + def get_spheres(self) -> List[List[int]]: + """ each sphere is { player: { location_id, ... } } """ + return self._multidata.get("spheres", []) + @app.route("/tracker///") def get_player_tracker(tracker: UUID, tracked_team: int, tracked_player: int, generic: bool = False) -> str: @@ -414,6 +419,26 @@ def render_generic_multiworld_tracker(tracker_data: TrackerData, enabled_tracker ) +def render_generic_multiworld_sphere_tracker(tracker_data: TrackerData) -> str: + return render_template( + "multispheretracker.html", + room=tracker_data.room, + tracker_data=tracker_data, + ) + + +@app.route("/sphere_tracker/") +@cache.memoize(timeout=TRACKER_CACHE_TIMEOUT_IN_SECONDS) +def get_multiworld_sphere_tracker(tracker: UUID): + # Room must exist. + room = Room.get(tracker=tracker) + if not room: + abort(404) + + tracker_data = TrackerData(room) + return render_generic_multiworld_sphere_tracker(tracker_data) + + # TODO: This is a temporary solution until a proper Tracker API can be implemented for tracker templates and data to # live in their respective world folders. From da33d1576abe56811d173350b85ab402704adf5f Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Sat, 1 Jun 2024 17:07:58 +0200 Subject: [PATCH 54/64] WebHost: update trackers only if they're visible. (#3407) --- WebHostLib/static/assets/trackerCommon.js | 85 ++++++++++-------- WebHostLib/templates/multitracker.html | 2 +- WebHostLib/tracker.py | 100 +++++++++++++++------- 3 files changed, 120 insertions(+), 67 deletions(-) diff --git a/WebHostLib/static/assets/trackerCommon.js b/WebHostLib/static/assets/trackerCommon.js index b8e089ece5d3..6324837b2816 100644 --- a/WebHostLib/static/assets/trackerCommon.js +++ b/WebHostLib/static/assets/trackerCommon.js @@ -27,7 +27,7 @@ const adjustTableHeight = () => { * @returns {string} */ const secondsToHours = (seconds) => { - let hours = Math.floor(seconds / 3600); + let hours = Math.floor(seconds / 3600); let minutes = Math.floor((seconds - (hours * 3600)) / 60).toString().padStart(2, '0'); return `${hours}:${minutes}`; }; @@ -38,18 +38,18 @@ window.addEventListener('load', () => { info: false, dom: "t", stateSave: true, - stateSaveCallback: function(settings, data) { + stateSaveCallback: function (settings, data) { delete data.search; localStorage.setItem(`DataTables_${settings.sInstance}_/tracker`, JSON.stringify(data)); }, - stateLoadCallback: function(settings) { + stateLoadCallback: function (settings) { return JSON.parse(localStorage.getItem(`DataTables_${settings.sInstance}_/tracker`)); }, - footerCallback: function(tfoot, data, start, end, display) { + footerCallback: function (tfoot, data, start, end, display) { if (tfoot) { const activityData = this.api().column('lastActivity:name').data().toArray().filter(x => !isNaN(x)); Array.from(tfoot?.children).find(td => td.classList.contains('last-activity')).innerText = - (activityData.length) ? secondsToHours(Math.min(...activityData)) : 'None'; + (activityData.length) ? secondsToHours(Math.min(...activityData)) : 'None'; } }, columnDefs: [ @@ -123,49 +123,64 @@ window.addEventListener('load', () => { event.preventDefault(); } }); - const tracker = document.getElementById('tracker-wrapper').getAttribute('data-tracker'); - const target_second = document.getElementById('tracker-wrapper').getAttribute('data-second') + 3; + const target_second = parseInt(document.getElementById('tracker-wrapper').getAttribute('data-second')) + 3; + console.log("Target second of refresh: " + target_second); - function getSleepTimeSeconds(){ + function getSleepTimeSeconds() { // -40 % 60 is -40, which is absolutely wrong and should burn var sleepSeconds = (((target_second - new Date().getSeconds()) % 60) + 60) % 60; return sleepSeconds || 60; } + let update_on_view = false; const update = () => { - const target = $("
"); - console.log("Updating Tracker..."); - target.load(location.href, function (response, status) { - if (status === "success") { - target.find(".table").each(function (i, new_table) { - const new_trs = $(new_table).find("tbody>tr"); - const footer_tr = $(new_table).find("tfoot>tr"); - const old_table = tables.eq(i); - const topscroll = $(old_table.settings()[0].nScrollBody).scrollTop(); - const leftscroll = $(old_table.settings()[0].nScrollBody).scrollLeft(); - old_table.clear(); - if (footer_tr.length) { - $(old_table.table).find("tfoot").html(footer_tr); - } - old_table.rows.add(new_trs); - old_table.draw(); - $(old_table.settings()[0].nScrollBody).scrollTop(topscroll); - $(old_table.settings()[0].nScrollBody).scrollLeft(leftscroll); - }); - $("#multi-stream-link").replaceWith(target.find("#multi-stream-link")); - } else { - console.log("Failed to connect to Server, in order to update Table Data."); - console.log(response); - } - }) - setTimeout(update, getSleepTimeSeconds()*1000); + if (document.hidden) { + console.log("Document reporting as not visible, not updating Tracker..."); + update_on_view = true; + } else { + update_on_view = false; + const target = $("
"); + console.log("Updating Tracker..."); + target.load(location.href, function (response, status) { + if (status === "success") { + target.find(".table").each(function (i, new_table) { + const new_trs = $(new_table).find("tbody>tr"); + const footer_tr = $(new_table).find("tfoot>tr"); + const old_table = tables.eq(i); + const topscroll = $(old_table.settings()[0].nScrollBody).scrollTop(); + const leftscroll = $(old_table.settings()[0].nScrollBody).scrollLeft(); + old_table.clear(); + if (footer_tr.length) { + $(old_table.table).find("tfoot").html(footer_tr); + } + old_table.rows.add(new_trs); + old_table.draw(); + $(old_table.settings()[0].nScrollBody).scrollTop(topscroll); + $(old_table.settings()[0].nScrollBody).scrollLeft(leftscroll); + }); + $("#multi-stream-link").replaceWith(target.find("#multi-stream-link")); + } else { + console.log("Failed to connect to Server, in order to update Table Data."); + console.log(response); + } + }) + } + updater = setTimeout(update, getSleepTimeSeconds() * 1000); } - setTimeout(update, getSleepTimeSeconds()*1000); + let updater = setTimeout(update, getSleepTimeSeconds() * 1000); window.addEventListener('resize', () => { adjustTableHeight(); tables.draw(); }); + window.addEventListener('visibilitychange', () => { + if (!document.hidden && update_on_view) { + console.log("Page became visible, tracker should be refreshed."); + clearTimeout(updater); + update(); + } + }); + adjustTableHeight(); }); diff --git a/WebHostLib/templates/multitracker.html b/WebHostLib/templates/multitracker.html index b16d4714ec6a..1b371b1229e5 100644 --- a/WebHostLib/templates/multitracker.html +++ b/WebHostLib/templates/multitracker.html @@ -10,7 +10,7 @@ {% include "header/dirtHeader.html" %} {% include "multitrackerNavigation.html" %} -
+
diff --git a/WebHostLib/tracker.py b/WebHostLib/tracker.py index 71ee9c7fcafe..36ebaacbcb0b 100644 --- a/WebHostLib/tracker.py +++ b/WebHostLib/tracker.py @@ -3,8 +3,9 @@ from dataclasses import dataclass from typing import Any, Callable, Dict, List, Optional, Set, Tuple, NamedTuple, Counter from uuid import UUID +from email.utils import parsedate_to_datetime -from flask import render_template +from flask import render_template, make_response, Response, request from werkzeug.exceptions import abort from MultiServer import Context, get_saving_second @@ -297,56 +298,92 @@ def get_spheres(self) -> List[List[int]]: return self._multidata.get("spheres", []) +def _process_if_request_valid(incoming_request, room: Optional[Room]) -> Optional[Response]: + if not room: + abort(404) + + if_modified = incoming_request.headers.get("If-Modified-Since", None) + if if_modified: + if_modified = parsedate_to_datetime(if_modified) + # if_modified has less precision than last_activity, so we bring them to same precision + if if_modified >= room.last_activity.replace(microsecond=0): + return make_response("", 304) + + @app.route("/tracker///") -def get_player_tracker(tracker: UUID, tracked_team: int, tracked_player: int, generic: bool = False) -> str: +def get_player_tracker(tracker: UUID, tracked_team: int, tracked_player: int, generic: bool = False) -> Response: key = f"{tracker}_{tracked_team}_{tracked_player}_{generic}" - tracker_page = cache.get(key) - if tracker_page: - return tracker_page + response: Optional[Response] = cache.get(key) + if response: + return response + + # Room must exist. + room = Room.get(tracker=tracker) + + response = _process_if_request_valid(request, room) + if response: + return response + + timeout, last_modified, tracker_page = get_timeout_and_player_tracker(room, tracked_team, tracked_player, generic) + response = make_response(tracker_page) + response.last_modified = last_modified + cache.set(key, response, timeout) + return response + + +def get_timeout_and_player_tracker(room: Room, tracked_team: int, tracked_player: int, generic: bool)\ + -> Tuple[int, datetime.datetime, str]: + tracker_data = TrackerData(room) + + # Load and render the game-specific player tracker, or fallback to generic tracker if none exists. + game_specific_tracker = _player_trackers.get(tracker_data.get_player_game(tracked_team, tracked_player), None) + if game_specific_tracker and not generic: + tracker = game_specific_tracker(tracker_data, tracked_team, tracked_player) + else: + tracker = render_generic_tracker(tracker_data, tracked_team, tracked_player) - timeout, tracker_page = get_timeout_and_tracker(tracker, tracked_team, tracked_player, generic) - cache.set(key, tracker_page, timeout) - return tracker_page + return ((tracker_data.get_room_saving_second() - datetime.datetime.now().second) + % TRACKER_CACHE_TIMEOUT_IN_SECONDS or TRACKER_CACHE_TIMEOUT_IN_SECONDS, room.last_activity, tracker) @app.route("/generic_tracker///") -def get_generic_game_tracker(tracker: UUID, tracked_team: int, tracked_player: int) -> str: +def get_generic_game_tracker(tracker: UUID, tracked_team: int, tracked_player: int) -> Response: return get_player_tracker(tracker, tracked_team, tracked_player, True) @app.route("/tracker/", defaults={"game": "Generic"}) @app.route("/tracker//") -@cache.memoize(timeout=TRACKER_CACHE_TIMEOUT_IN_SECONDS) -def get_multiworld_tracker(tracker: UUID, game: str): +def get_multiworld_tracker(tracker: UUID, game: str) -> Response: + key = f"{tracker}_{game}" + response: Optional[Response] = cache.get(key) + if response: + return response + # Room must exist. room = Room.get(tracker=tracker) - if not room: - abort(404) - tracker_data = TrackerData(room) - enabled_trackers = list(get_enabled_multiworld_trackers(room).keys()) - if game not in _multiworld_trackers: - return render_generic_multiworld_tracker(tracker_data, enabled_trackers) + response = _process_if_request_valid(request, room) + if response: + return response - return _multiworld_trackers[game](tracker_data, enabled_trackers) + timeout, last_modified, tracker_page = get_timeout_and_multiworld_tracker(room, game) + response = make_response(tracker_page) + response.last_modified = last_modified + cache.set(key, response, timeout) + return response -def get_timeout_and_tracker(tracker: UUID, tracked_team: int, tracked_player: int, generic: bool) -> Tuple[int, str]: - # Room must exist. - room = Room.get(tracker=tracker) - if not room: - abort(404) - +def get_timeout_and_multiworld_tracker(room: Room, game: str)\ + -> Tuple[int, datetime.datetime, str]: tracker_data = TrackerData(room) - - # Load and render the game-specific player tracker, or fallback to generic tracker if none exists. - game_specific_tracker = _player_trackers.get(tracker_data.get_player_game(tracked_team, tracked_player), None) - if game_specific_tracker and not generic: - tracker = game_specific_tracker(tracker_data, tracked_team, tracked_player) + enabled_trackers = list(get_enabled_multiworld_trackers(room).keys()) + if game in _multiworld_trackers: + tracker = _multiworld_trackers[game](tracker_data, enabled_trackers) else: - tracker = render_generic_tracker(tracker_data, tracked_team, tracked_player) + tracker = render_generic_multiworld_tracker(tracker_data, enabled_trackers) - return (tracker_data.get_room_saving_second() - datetime.datetime.now().second) % 60 or 60, tracker + return ((tracker_data.get_room_saving_second() - datetime.datetime.now().second) + % TRACKER_CACHE_TIMEOUT_IN_SECONDS or TRACKER_CACHE_TIMEOUT_IN_SECONDS, room.last_activity, tracker) def get_enabled_multiworld_trackers(room: Room) -> Dict[str, Callable]: @@ -416,6 +453,7 @@ def render_generic_multiworld_tracker(tracker_data: TrackerData, enabled_tracker videos=tracker_data.get_room_videos(), item_id_to_name=tracker_data.item_id_to_name, location_id_to_name=tracker_data.location_id_to_name, + saving_second=tracker_data.get_room_saving_second(), ) From e49b1f9fbb5e8eb98882cc4307541253e5f23c7f Mon Sep 17 00:00:00 2001 From: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com> Date: Sat, 1 Jun 2024 23:11:28 +0200 Subject: [PATCH 55/64] The Witness: Automatic Postgame & Disabled Panels Calculation (#2698) * Refactor postgame code to be more readable * Change all references to options to strings * oops * Fix some outdated code related to yaml-disabled EPs * Small fixes to short/longbox stuff (thanks Medic) * comment * fix duplicate * Removed triplicate lmfao * Better comment * added another 'unfun' postgame consideration * comment * more option strings * oops * Remove an unnecessary comparison * another string missed * New classification changes (Credit: Exempt-Medic) * Don't need to pass world * Comments * Replace it with another magic system because why not at this point :DDDDDD * oops * Oops * Another was missed * Make events conditions. Disable_Non_Randomized will no longer just 'have all events' * What the fuck? Has this just always been broken? * Don't have boolean function with 'not' in the name * Another useful classification * slight code refactor * Funny haha booleans * This would create a really bad merge error * I can't believe this actually kind of works * And here's the punchline. + some bugfixes * Comment dat code * Comments galore * LMAO OOPS * so nice I did it twice * debug x2 * Careful * Add more comments * That comment is a bit unnecessary now * Fix overriding region connections * Correct a comment * Correct again * Rename variable * Idk I guess this is in this branch now * More tweaking of postgame & comments * This is commit just exists to fix that grammar error * I think I can just fucking delete this now??? * Forgot to reset something here * Delete dead codepath * Obelisk Keys were getting yote erroneously * More comments * Fix duplicate connections * Oopsington III * performance improvements & cleanup * More rules cleanup and performance improvements * Oh cool I can do this huh * Okay but this is even more swag tho * Lazy eval * remove some implicit checks * Is this too magical yet * more guard magic * Maaaaaaaagiccccccccc * Laaaaaaaaaaaaaaaazzzzzzyyyyyyyyyyy * Make it docstring * Newline bc I like that better * this is a little spooky lol * lol * Wait * spoO * Better variable name and comment * Improved comment again * better API * oops I deleted a deepcopy * lol help * Help??? * player_regionsns lmao * Add some comments * Make doors disabled properly again. I hope this works * Don't disable lasers * Omega oops * Make Floor 2 Exit not exist * Make a fix that's warps compatible * I think this was an oversight, I tested a seed and it seems to have the same result * This is definitely less Violet than before * Does this feel more violet lol * Exception if a laser gets disabled, cleanup * Ruff * >:( * consistent utils import * Make autopostgame more reviewable (hopefully) * more reviewability * WitnessRule * replace another instance of it * lint * style * comment * found the bug * Move comment * Get rid of cache and ugly allow_victory * comments and lint --- worlds/witness/data/WitnessLogic.txt | 4 +- worlds/witness/data/WitnessLogicExpert.txt | 4 +- worlds/witness/data/WitnessLogicVanilla.txt | 4 +- .../Caves_Except_Path_To_Challenge.txt} | 0 .../Exclusions/Disable_Unrandomized.txt | 14 - .../data/settings/Exclusions/Vaults.txt | 23 - .../settings/Postgame/Beyond_Challenge.txt | 4 - .../Postgame/Bottom_Floor_Discard.txt | 2 - .../Bottom_Floor_Discard_NonDoors.txt | 6 - .../settings/Postgame/Challenge_Vault_Box.txt | 22 - .../data/settings/Postgame/Mountain_Lower.txt | 27 - .../data/settings/Postgame/Mountain_Upper.txt | 41 -- .../settings/Postgame/Path_To_Challenge.txt | 30 - worlds/witness/data/static_logic.py | 51 +- worlds/witness/data/utils.py | 66 +- worlds/witness/hints.py | 4 +- worlds/witness/player_logic.py | 580 ++++++++++++------ worlds/witness/regions.py | 63 +- worlds/witness/rules.py | 186 ++++-- 19 files changed, 628 insertions(+), 503 deletions(-) rename worlds/witness/data/settings/{Postgame/Caves.txt => Exclusions/Caves_Except_Path_To_Challenge.txt} (100%) delete mode 100644 worlds/witness/data/settings/Postgame/Beyond_Challenge.txt delete mode 100644 worlds/witness/data/settings/Postgame/Bottom_Floor_Discard.txt delete mode 100644 worlds/witness/data/settings/Postgame/Bottom_Floor_Discard_NonDoors.txt delete mode 100644 worlds/witness/data/settings/Postgame/Challenge_Vault_Box.txt delete mode 100644 worlds/witness/data/settings/Postgame/Mountain_Lower.txt delete mode 100644 worlds/witness/data/settings/Postgame/Mountain_Upper.txt delete mode 100644 worlds/witness/data/settings/Postgame/Path_To_Challenge.txt diff --git a/worlds/witness/data/WitnessLogic.txt b/worlds/witness/data/WitnessLogic.txt index 6a89a8b060e8..272ed176e342 100644 --- a/worlds/witness/data/WitnessLogic.txt +++ b/worlds/witness/data/WitnessLogic.txt @@ -1028,7 +1028,7 @@ Mountain Floor 1 Bridge (Mountain Floor 1) - Mountain Floor 1 At Door - TrueOneW Mountain Floor 1 At Door (Mountain Floor 1) - Mountain Floor 2 - 0x09E54: Door - 0x09E54 (Exit) - 0x09EAF & 0x09F6E & 0x09E6B & 0x09E7B -Mountain Floor 2 (Mountain Floor 2) - Mountain Floor 2 Light Bridge Room Near - 0x09FFB - Mountain Floor 2 Beyond Bridge - 0x09E86 - Mountain Floor 2 At Door - 0x09ED8 & 0x09E86 - Mountain Pink Bridge EP - TrueOneWay: +Mountain Floor 2 (Mountain Floor 2) - Mountain Floor 2 Light Bridge Room Near - 0x09FFB - Mountain Floor 2 Beyond Bridge - 0x09E86 - Mountain Floor 2 Above The Abyss - True - Mountain Pink Bridge EP - TrueOneWay: 158426 - 0x09FD3 (Near Row 1) - True - Stars & Colored Squares & Stars + Same Colored Symbol 158427 - 0x09FD4 (Near Row 2) - 0x09FD3 - Stars & Colored Squares & Stars + Same Colored Symbol 158428 - 0x09FD6 (Near Row 3) - 0x09FD4 - Stars & Colored Squares & Stars + Same Colored Symbol @@ -1036,7 +1036,7 @@ Mountain Floor 2 (Mountain Floor 2) - Mountain Floor 2 Light Bridge Room Near - 158430 - 0x09FD8 (Near Row 5) - 0x09FD7 - Symmetry & Colored Dots Door - 0x09FFB (Staircase Near) - 0x09FD8 -Mountain Floor 2 At Door (Mountain Floor 2) - Mountain Floor 2 Elevator Room - 0x09EDD: +Mountain Floor 2 Above The Abyss (Mountain Floor 2) - Mountain Floor 2 Elevator Room - 0x09EDD & 0x09ED8 & 0x09E86: Door - 0x09EDD (Elevator Room Entry) - 0x09ED8 & 0x09E86 Mountain Floor 2 Light Bridge Room Near (Mountain Floor 2): diff --git a/worlds/witness/data/WitnessLogicExpert.txt b/worlds/witness/data/WitnessLogicExpert.txt index 7a8c37ac309e..63e7e36c243e 100644 --- a/worlds/witness/data/WitnessLogicExpert.txt +++ b/worlds/witness/data/WitnessLogicExpert.txt @@ -1028,7 +1028,7 @@ Mountain Floor 1 Bridge (Mountain Floor 1) - Mountain Floor 1 At Door - TrueOneW Mountain Floor 1 At Door (Mountain Floor 1) - Mountain Floor 2 - 0x09E54: Door - 0x09E54 (Exit) - 0x09EAF & 0x09F6E & 0x09E6B & 0x09E7B -Mountain Floor 2 (Mountain Floor 2) - Mountain Floor 2 Light Bridge Room Near - 0x09FFB - Mountain Floor 2 Beyond Bridge - 0x09E86 - Mountain Floor 2 At Door - 0x09ED8 & 0x09E86 - Mountain Pink Bridge EP - TrueOneWay: +Mountain Floor 2 (Mountain Floor 2) - Mountain Floor 2 Light Bridge Room Near - 0x09FFB - Mountain Floor 2 Beyond Bridge - 0x09E86 - Mountain Floor 2 Above The Abyss - True - Mountain Pink Bridge EP - TrueOneWay: 158426 - 0x09FD3 (Near Row 1) - True - Stars & Colored Squares & Stars + Same Colored Symbol 158427 - 0x09FD4 (Near Row 2) - 0x09FD3 - Stars & Triangles & Stars + Same Colored Symbol 158428 - 0x09FD6 (Near Row 3) - 0x09FD4 - Stars & Shapers & Negative Shapers & Stars + Same Colored Symbol @@ -1036,7 +1036,7 @@ Mountain Floor 2 (Mountain Floor 2) - Mountain Floor 2 Light Bridge Room Near - 158430 - 0x09FD8 (Near Row 5) - 0x09FD7 - Stars & Stars + Same Colored Symbol & Rotated Shapers & Eraser Door - 0x09FFB (Staircase Near) - 0x09FD8 -Mountain Floor 2 At Door (Mountain Floor 2) - Mountain Floor 2 Elevator Room - 0x09EDD: +Mountain Floor 2 Above The Abyss (Mountain Floor 2) - Mountain Floor 2 Elevator Room - 0x09EDD & 0x09ED8 & 0x09E86: Door - 0x09EDD (Elevator Room Entry) - 0x09ED8 & 0x09E86 Mountain Floor 2 Light Bridge Room Near (Mountain Floor 2): diff --git a/worlds/witness/data/WitnessLogicVanilla.txt b/worlds/witness/data/WitnessLogicVanilla.txt index 84205030cc64..1aa9655361f9 100644 --- a/worlds/witness/data/WitnessLogicVanilla.txt +++ b/worlds/witness/data/WitnessLogicVanilla.txt @@ -1028,7 +1028,7 @@ Mountain Floor 1 Bridge (Mountain Floor 1) - Mountain Floor 1 At Door - TrueOneW Mountain Floor 1 At Door (Mountain Floor 1) - Mountain Floor 2 - 0x09E54: Door - 0x09E54 (Exit) - 0x09EAF & 0x09F6E & 0x09E6B & 0x09E7B -Mountain Floor 2 (Mountain Floor 2) - Mountain Floor 2 Light Bridge Room Near - 0x09FFB - Mountain Floor 2 Beyond Bridge - 0x09E86 - Mountain Floor 2 At Door - 0x09ED8 & 0x09E86 - Mountain Pink Bridge EP - TrueOneWay: +Mountain Floor 2 (Mountain Floor 2) - Mountain Floor 2 Light Bridge Room Near - 0x09FFB - Mountain Floor 2 Beyond Bridge - 0x09E86 - Mountain Floor 2 Above The Abyss - True - Mountain Pink Bridge EP - TrueOneWay: 158426 - 0x09FD3 (Near Row 1) - True - Colored Squares 158427 - 0x09FD4 (Near Row 2) - 0x09FD3 - Colored Squares & Dots 158428 - 0x09FD6 (Near Row 3) - 0x09FD4 - Stars & Colored Squares & Stars + Same Colored Symbol @@ -1036,7 +1036,7 @@ Mountain Floor 2 (Mountain Floor 2) - Mountain Floor 2 Light Bridge Room Near - 158430 - 0x09FD8 (Near Row 5) - 0x09FD7 - Colored Squares Door - 0x09FFB (Staircase Near) - 0x09FD8 -Mountain Floor 2 At Door (Mountain Floor 2) - Mountain Floor 2 Elevator Room - 0x09EDD: +Mountain Floor 2 Above The Abyss (Mountain Floor 2) - Mountain Floor 2 Elevator Room - 0x09EDD & 0x09ED8 & 0x09E86: Door - 0x09EDD (Elevator Room Entry) - 0x09ED8 & 0x09E86 Mountain Floor 2 Light Bridge Room Near (Mountain Floor 2): diff --git a/worlds/witness/data/settings/Postgame/Caves.txt b/worlds/witness/data/settings/Exclusions/Caves_Except_Path_To_Challenge.txt similarity index 100% rename from worlds/witness/data/settings/Postgame/Caves.txt rename to worlds/witness/data/settings/Exclusions/Caves_Except_Path_To_Challenge.txt diff --git a/worlds/witness/data/settings/Exclusions/Disable_Unrandomized.txt b/worlds/witness/data/settings/Exclusions/Disable_Unrandomized.txt index 09c366cfaabd..3dfc34e8ad0a 100644 --- a/worlds/witness/data/settings/Exclusions/Disable_Unrandomized.txt +++ b/worlds/witness/data/settings/Exclusions/Disable_Unrandomized.txt @@ -134,17 +134,3 @@ Disabled Locations: 0x17E67 (Bunker UV Room 2) 0x09DE0 (Bunker Laser) 0x0A079 (Bunker Elevator Control) - -0x034A7 (Monastery Left Shutter EP) -0x034AD (Monastery Middle Shutter EP) -0x034AF (Monastery Right Shutter EP) -0x339B6 (Theater Eclipse EP) -0x33A29 (Theater Window EP) -0x33A2A (Theater Door EP) -0x33B06 (Theater Church EP) -0x3352F (Tutorial Gate EP) -0x33600 (Tutorial Patio Flowers EP) -0x035F5 (Bunker Tinted Door EP) -0x000D3 (Bunker Green Room Flowers EP) -0x33A20 (Theater Flowers EP) -0x03BE2 (Monastery Garden Left EP) diff --git a/worlds/witness/data/settings/Exclusions/Vaults.txt b/worlds/witness/data/settings/Exclusions/Vaults.txt index d9e5d28cd694..9eade5e52855 100644 --- a/worlds/witness/data/settings/Exclusions/Vaults.txt +++ b/worlds/witness/data/settings/Exclusions/Vaults.txt @@ -1,31 +1,8 @@ Disabled Locations: 0x033D4 (Outside Tutorial Vault) -0x03481 (Outside Tutorial Vault Box) -0x033D0 (Outside Tutorial Vault Door) 0x0CC7B (Desert Vault) -0x0339E (Desert Vault Box) -0x03444 (Desert Vault Door) 0x00AFB (Shipwreck Vault) -0x03535 (Shipwreck Vault Box) -0x17BB4 (Shipwreck Vault Door) 0x15ADD (Jungle Vault) -0x03702 (Jungle Vault Box) -0x15287 (Jungle Vault Door) 0x002A6 (Mountainside Vault) -0x03542 (Mountainside Vault Box) -0x00085 (Mountainside Vault Door) 0x2FAF6 (Tunnels Vault Box) 0x00815 (Theater Video Input) -0x03553 (Theater Tutorial Video) -0x03552 (Theater Desert Video) -0x0354E (Theater Jungle Video) -0x03549 (Theater Challenge Video) -0x0354F (Theater Shipwreck Video) -0x03545 (Theater Mountain Video) -0x03505 (Tutorial Gate Close) -0x339B6 (Theater clipse EP) -0x33A29 (Theater Window EP) -0x33A2A (Theater Door EP) -0x33B06 (Theater Church EP) -0x33A20 (Theater Flowers EP) -0x3352F (Tutorial Gate EP) diff --git a/worlds/witness/data/settings/Postgame/Beyond_Challenge.txt b/worlds/witness/data/settings/Postgame/Beyond_Challenge.txt deleted file mode 100644 index 5cd20b6a5e40..000000000000 --- a/worlds/witness/data/settings/Postgame/Beyond_Challenge.txt +++ /dev/null @@ -1,4 +0,0 @@ -Disabled Locations: -0x03549 (Challenge Video) - -0x339B6 (Eclipse EP) diff --git a/worlds/witness/data/settings/Postgame/Bottom_Floor_Discard.txt b/worlds/witness/data/settings/Postgame/Bottom_Floor_Discard.txt deleted file mode 100644 index 8f7d6a257a53..000000000000 --- a/worlds/witness/data/settings/Postgame/Bottom_Floor_Discard.txt +++ /dev/null @@ -1,2 +0,0 @@ -Disabled Locations: -0x17FA2 (Mountain Bottom Floor Discard) diff --git a/worlds/witness/data/settings/Postgame/Bottom_Floor_Discard_NonDoors.txt b/worlds/witness/data/settings/Postgame/Bottom_Floor_Discard_NonDoors.txt deleted file mode 100644 index 5ea7c578d8bf..000000000000 --- a/worlds/witness/data/settings/Postgame/Bottom_Floor_Discard_NonDoors.txt +++ /dev/null @@ -1,6 +0,0 @@ -Disabled Locations: -0x17FA2 (Mountain Bottom Floor Discard) -0x17F33 (Rock Open Door) -0x00FF8 (Caves Entry Panel) -0x334E1 (Rock Control) -0x2D77D (Caves Entry Door) diff --git a/worlds/witness/data/settings/Postgame/Challenge_Vault_Box.txt b/worlds/witness/data/settings/Postgame/Challenge_Vault_Box.txt deleted file mode 100644 index 8b431694b3b4..000000000000 --- a/worlds/witness/data/settings/Postgame/Challenge_Vault_Box.txt +++ /dev/null @@ -1,22 +0,0 @@ -Disabled Locations: -0x0356B (Challenge Vault Box) -0x04D75 (Vault Door) -0x0A332 (Start Timer) -0x0088E (Small Basic) -0x00BAF (Big Basic) -0x00BF3 (Square) -0x00C09 (Maze Map) -0x00CDB (Stars and Dots) -0x0051F (Symmetry) -0x00524 (Stars and Shapers) -0x00CD4 (Big Basic 2) -0x00CB9 (Choice Squares Right) -0x00CA1 (Choice Squares Middle) -0x00C80 (Choice Squares Left) -0x00C68 (Choice Squares 2 Right) -0x00C59 (Choice Squares 2 Middle) -0x00C22 (Choice Squares 2 Left) -0x034F4 (Maze Hidden 1) -0x034EC (Maze Hidden 2) -0x1C31A (Dots Pillar) -0x1C319 (Squares Pillar) diff --git a/worlds/witness/data/settings/Postgame/Mountain_Lower.txt b/worlds/witness/data/settings/Postgame/Mountain_Lower.txt deleted file mode 100644 index aecddec5adde..000000000000 --- a/worlds/witness/data/settings/Postgame/Mountain_Lower.txt +++ /dev/null @@ -1,27 +0,0 @@ -Disabled Locations: -0x17F93 (Elevator Discard) -0x09EEB (Elevator Control Panel) -0x09FC1 (Giant Puzzle Bottom Left) -0x09F8E (Giant Puzzle Bottom Right) -0x09F01 (Giant Puzzle Top Right) -0x09EFF (Giant Puzzle Top Left) -0x09FDA (Giant Puzzle) -0x09F89 (Exit Door) -0x01983 (Pillars Room Entry Left) -0x01987 (Pillars Room Entry Right) -0x0C141 (Pillars Room Entry Door) -0x0383A (Right Pillar 1) -0x09E56 (Right Pillar 2) -0x09E5A (Right Pillar 3) -0x33961 (Right Pillar 4) -0x0383D (Left Pillar 1) -0x0383F (Left Pillar 2) -0x03859 (Left Pillar 3) -0x339BB (Left Pillar 4) -0x3D9A6 (Elevator Door Closer Left) -0x3D9A7 (Elevator Door Close Right) -0x3C113 (Elevator Entry Left) -0x3C114 (Elevator Entry Right) -0x3D9AA (Back Wall Left) -0x3D9A8 (Back Wall Right) -0x3D9A9 (Elevator Start) diff --git a/worlds/witness/data/settings/Postgame/Mountain_Upper.txt b/worlds/witness/data/settings/Postgame/Mountain_Upper.txt deleted file mode 100644 index e2b0765f533c..000000000000 --- a/worlds/witness/data/settings/Postgame/Mountain_Upper.txt +++ /dev/null @@ -1,41 +0,0 @@ -Disabled Locations: -0x17C34 (Mountain Entry Panel) -0x09E39 (Light Bridge Controller) -0x09E7A (Right Row 1) -0x09E71 (Right Row 2) -0x09E72 (Right Row 3) -0x09E69 (Right Row 4) -0x09E7B (Right Row 5) -0x09E73 (Left Row 1) -0x09E75 (Left Row 2) -0x09E78 (Left Row 3) -0x09E79 (Left Row 4) -0x09E6C (Left Row 5) -0x09E6F (Left Row 6) -0x09E6B (Left Row 7) -0x33AF5 (Back Row 1) -0x33AF7 (Back Row 2) -0x09F6E (Back Row 3) -0x09EAD (Trash Pillar 1) -0x09EAF (Trash Pillar 2) -0x09E54 (Mountain Floor 1 Exit Door) -0x09FD3 (Near Row 1) -0x09FD4 (Near Row 2) -0x09FD6 (Near Row 3) -0x09FD7 (Near Row 4) -0x09FD8 (Near Row 5) -0x09FFB (Staircase Near Door) -0x09EDD (Elevator Room Entry Door) -0x09E86 (Light Bridge Controller Near) -0x09FCC (Far Row 1) -0x09FCE (Far Row 2) -0x09FCF (Far Row 3) -0x09FD0 (Far Row 4) -0x09FD1 (Far Row 5) -0x09FD2 (Far Row 6) -0x09E07 (Staircase Far Door) -0x09ED8 (Light Bridge Controller Far) - -0x09D63 (Pink Bridge EP) -0x09D5D (Yellow Bridge EP) -0x09D5E (Blue Bridge EP) diff --git a/worlds/witness/data/settings/Postgame/Path_To_Challenge.txt b/worlds/witness/data/settings/Postgame/Path_To_Challenge.txt deleted file mode 100644 index 3f9239cc4832..000000000000 --- a/worlds/witness/data/settings/Postgame/Path_To_Challenge.txt +++ /dev/null @@ -1,30 +0,0 @@ -Disabled Locations: -0x0356B (Vault Box) -0x04D75 (Vault Door) -0x17F33 (Rock Open Door) -0x00FF8 (Caves Entry Panel) -0x334E1 (Rock Control) -0x2D77D (Caves Entry Door) -0x09DD5 (Lone Pillar) -0x019A5 (Caves Pillar Door) -0x0A16E (Challenge Entry Panel) -0x0A19A (Challenge Entry Door) -0x0A332 (Start Timer) -0x0088E (Small Basic) -0x00BAF (Big Basic) -0x00BF3 (Square) -0x00C09 (Maze Map) -0x00CDB (Stars and Dots) -0x0051F (Symmetry) -0x00524 (Stars and Shapers) -0x00CD4 (Big Basic 2) -0x00CB9 (Choice Squares Right) -0x00CA1 (Choice Squares Middle) -0x00C80 (Choice Squares Left) -0x00C68 (Choice Squares 2 Right) -0x00C59 (Choice Squares 2 Middle) -0x00C22 (Choice Squares 2 Left) -0x034F4 (Maze Hidden 1) -0x034EC (Maze Hidden 2) -0x1C31A (Dots Pillar) -0x1C319 (Squares Pillar) diff --git a/worlds/witness/data/static_logic.py b/worlds/witness/data/static_logic.py index 94e6f7a3cc97..bae1921f6095 100644 --- a/worlds/witness/data/static_logic.py +++ b/worlds/witness/data/static_logic.py @@ -1,5 +1,6 @@ +from collections import defaultdict from functools import lru_cache -from typing import Dict, List +from typing import Dict, List, Set, Tuple from .item_definition_classes import ( CATEGORY_NAME_MAPPINGS, @@ -10,11 +11,13 @@ WeightedItemDefinition, ) from .utils import ( + WitnessRule, define_new_region, get_items, get_sigma_expert_logic, get_sigma_normal_logic, get_vanilla_logic, + logical_or_witness_rules, parse_lambda, ) @@ -41,7 +44,8 @@ def read_logic_file(self, lines) -> None: current_region = new_region_and_connections[0] region_name = current_region["name"] self.ALL_REGIONS_BY_NAME[region_name] = current_region - self.STATIC_CONNECTIONS_BY_REGION_NAME[region_name] = new_region_and_connections[1] + for connection in new_region_and_connections[1]: + self.CONNECTIONS_WITH_DUPLICATES[region_name][connection[0]].add(connection[1]) current_area["regions"].append(region_name) continue @@ -80,13 +84,15 @@ def read_logic_file(self, lines) -> None: self.ENTITIES_BY_NAME[self.ENTITIES_BY_HEX[entity_hex]["checkName"]] = self.ENTITIES_BY_HEX[entity_hex] self.STATIC_DEPENDENT_REQUIREMENTS_BY_HEX[entity_hex] = { - "panels": parse_lambda(required_panel_lambda) + "entities": parse_lambda(required_panel_lambda) } # Lasers and Doors exist in a region, but don't have a regional *requirement* # If a laser is activated, you don't need to physically walk up to it for it to count # As such, logically, they behave more as if they were part of the "Entry" region - self.ALL_REGIONS_BY_NAME["Entry"]["panels"].append(entity_hex) + self.ALL_REGIONS_BY_NAME["Entry"]["entities"].append(entity_hex) + # However, it will also be important to keep track of their physical location for postgame purposes. + current_region["physical_entities"].append(entity_hex) continue required_item_lambda = line_split.pop(0) @@ -117,7 +123,7 @@ def read_logic_file(self, lines) -> None: required_items = frozenset(required_items) requirement = { - "panels": required_panels, + "entities": required_panels, "items": required_items } @@ -145,7 +151,37 @@ def read_logic_file(self, lines) -> None: self.ENTITIES_BY_NAME[self.ENTITIES_BY_HEX[entity_hex]["checkName"]] = self.ENTITIES_BY_HEX[entity_hex] self.STATIC_DEPENDENT_REQUIREMENTS_BY_HEX[entity_hex] = requirement - current_region["panels"].append(entity_hex) + current_region["entities"].append(entity_hex) + current_region["physical_entities"].append(entity_hex) + + def reverse_connection(self, source_region: str, connection: Tuple[str, Set[WitnessRule]]): + target = connection[0] + traversal_options = connection[1] + + # Reverse this connection with all its possibilities, except the ones marked as "OneWay". + for requirement in traversal_options: + remaining_options = set() + for option in requirement: + if not any(req == "TrueOneWay" for req in option): + remaining_options.add(option) + + if remaining_options: + self.CONNECTIONS_WITH_DUPLICATES[target][source_region].add(frozenset(remaining_options)) + + def reverse_connections(self): + # Iterate all connections + for region_name, connections in list(self.CONNECTIONS_WITH_DUPLICATES.items()): + for connection in connections.items(): + self.reverse_connection(region_name, connection) + + def combine_connections(self): + # All regions need to be present, and this dict is copied later - Thus, defaultdict is not the correct choice. + self.STATIC_CONNECTIONS_BY_REGION_NAME = {region_name: set() for region_name in self.ALL_REGIONS_BY_NAME} + + for source, connections in self.CONNECTIONS_WITH_DUPLICATES.items(): + for target, requirement in connections.items(): + combined_req = logical_or_witness_rules(requirement) + self.STATIC_CONNECTIONS_BY_REGION_NAME[source].add((target, combined_req)) def __init__(self, lines=None) -> None: if lines is None: @@ -154,6 +190,7 @@ def __init__(self, lines=None) -> None: # All regions with a list of panels in them and the connections to other regions, before logic adjustments self.ALL_REGIONS_BY_NAME = dict() self.ALL_AREAS_BY_NAME = dict() + self.CONNECTIONS_WITH_DUPLICATES = defaultdict(lambda: defaultdict(lambda: set())) self.STATIC_CONNECTIONS_BY_REGION_NAME = dict() self.ENTITIES_BY_HEX = dict() @@ -167,6 +204,8 @@ def __init__(self, lines=None) -> None: self.ENTITY_ID_TO_NAME = dict() self.read_logic_file(lines) + self.reverse_connections() + self.combine_connections() # Item data parsed from WitnessItems.txt diff --git a/worlds/witness/data/utils.py b/worlds/witness/data/utils.py index bb89227ca37f..5c5568b25661 100644 --- a/worlds/witness/data/utils.py +++ b/worlds/witness/data/utils.py @@ -2,7 +2,14 @@ from math import floor from pkgutil import get_data from random import random -from typing import Any, Collection, Dict, FrozenSet, List, Set, Tuple +from typing import Any, Collection, Dict, FrozenSet, Iterable, List, Set, Tuple + +# A WitnessRule is just an or-chain of and-conditions. +# It represents the set of all options that could fulfill this requirement. +# E.g. if something requires "Dots or (Shapers and Stars)", it'd be represented as: {{"Dots"}, {"Shapers, "Stars"}} +# {} is an unusable requirement. +# {{}} is an always usable requirement. +WitnessRule = FrozenSet[FrozenSet[str]] def weighted_sample(world_random: random, population: List, weights: List[float], k: int) -> List: @@ -48,7 +55,7 @@ def build_weighted_int_list(inputs: Collection[float], total: int) -> List[int]: return rounded_output -def define_new_region(region_string: str) -> Tuple[Dict[str, Any], Set[Tuple[str, FrozenSet[FrozenSet[str]]]]]: +def define_new_region(region_string: str) -> Tuple[Dict[str, Any], Set[Tuple[str, WitnessRule]]]: """ Returns a region object by parsing a line in the logic file """ @@ -76,12 +83,13 @@ def define_new_region(region_string: str) -> Tuple[Dict[str, Any], Set[Tuple[str region_obj = { "name": region_name, "shortName": region_name_simple, - "panels": list() + "entities": list(), + "physical_entities": list(), } return region_obj, options -def parse_lambda(lambda_string) -> FrozenSet[FrozenSet[str]]: +def parse_lambda(lambda_string) -> WitnessRule: """ Turns a lambda String literal like this: a | b & c into a set of sets like this: {{a}, {b, c}} @@ -181,36 +189,8 @@ def get_discard_exclusion_list() -> List[str]: return get_adjustment_file("settings/Exclusions/Discards.txt") -def get_caves_exclusion_list() -> List[str]: - return get_adjustment_file("settings/Postgame/Caves.txt") - - -def get_beyond_challenge_exclusion_list() -> List[str]: - return get_adjustment_file("settings/Postgame/Beyond_Challenge.txt") - - -def get_bottom_floor_discard_exclusion_list() -> List[str]: - return get_adjustment_file("settings/Postgame/Bottom_Floor_Discard.txt") - - -def get_bottom_floor_discard_nondoors_exclusion_list() -> List[str]: - return get_adjustment_file("settings/Postgame/Bottom_Floor_Discard_NonDoors.txt") - - -def get_mountain_upper_exclusion_list() -> List[str]: - return get_adjustment_file("settings/Postgame/Mountain_Upper.txt") - - -def get_challenge_vault_box_exclusion_list() -> List[str]: - return get_adjustment_file("settings/Postgame/Challenge_Vault_Box.txt") - - -def get_path_to_challenge_exclusion_list() -> List[str]: - return get_adjustment_file("settings/Postgame/Path_To_Challenge.txt") - - -def get_mountain_lower_exclusion_list() -> List[str]: - return get_adjustment_file("settings/Postgame/Mountain_Lower.txt") +def get_caves_except_path_to_challenge_exclusion_list() -> List[str]: + return get_adjustment_file("settings/Exclusions/Caves_Except_Path_To_Challenge.txt") def get_elevators_come_to_you() -> List[str]: @@ -233,21 +213,21 @@ def get_items() -> List[str]: return get_adjustment_file("WitnessItems.txt") -def dnf_remove_redundancies(dnf_requirement: FrozenSet[FrozenSet[str]]) -> FrozenSet[FrozenSet[str]]: +def optimize_witness_rule(witness_rule: WitnessRule) -> WitnessRule: """Removes any redundant terms from a logical formula in disjunctive normal form. This means removing any terms that are a superset of any other term get removed. This is possible because of the boolean absorption law: a | (a & b) = a""" to_remove = set() - for option1 in dnf_requirement: - for option2 in dnf_requirement: + for option1 in witness_rule: + for option2 in witness_rule: if option2 < option1: to_remove.add(option1) - return dnf_requirement - to_remove + return witness_rule - to_remove -def dnf_and(dnf_requirements: List[FrozenSet[FrozenSet[str]]]) -> FrozenSet[FrozenSet[str]]: +def logical_and_witness_rules(witness_rules: Iterable[WitnessRule]) -> WitnessRule: """ performs the "and" operator on a list of logical formula in disjunctive normal form, represented as a set of sets. A logical formula might look like this: {{a, b}, {c, d}}, which would mean "a & b | c & d". @@ -255,7 +235,7 @@ def dnf_and(dnf_requirements: List[FrozenSet[FrozenSet[str]]]) -> FrozenSet[Froz """ current_overall_requirement = frozenset({frozenset()}) - for next_dnf_requirement in dnf_requirements: + for next_dnf_requirement in witness_rules: new_requirement: Set[FrozenSet[str]] = set() for option1 in current_overall_requirement: @@ -264,4 +244,8 @@ def dnf_and(dnf_requirements: List[FrozenSet[FrozenSet[str]]]) -> FrozenSet[Froz current_overall_requirement = frozenset(new_requirement) - return dnf_remove_redundancies(current_overall_requirement) + return optimize_witness_rule(current_overall_requirement) + + +def logical_or_witness_rules(witness_rules: Iterable[WitnessRule]) -> WitnessRule: + return optimize_witness_rule(frozenset.union(*witness_rules)) diff --git a/worlds/witness/hints.py b/worlds/witness/hints.py index fa6f658b451d..535a36e13b6f 100644 --- a/worlds/witness/hints.py +++ b/worlds/witness/hints.py @@ -373,9 +373,9 @@ def get_hintable_areas(world: "WitnessWorld") -> Tuple[Dict[str, List[Location]] for area in potential_areas: regions = [ - world.player_regions.created_regions[region] + world.get_region(region) for region in static_witness_logic.ALL_AREAS_BY_NAME[area]["regions"] - if region in world.player_regions.created_regions + if region in world.player_regions.created_region_names ] locations = [location for region in regions for location in region.get_locations() if location.address] diff --git a/worlds/witness/player_logic.py b/worlds/witness/player_logic.py index 01caee89515b..4335f9524f1e 100644 --- a/worlds/witness/player_logic.py +++ b/worlds/witness/player_logic.py @@ -17,13 +17,39 @@ import copy from collections import defaultdict -from functools import lru_cache from logging import warning -from typing import TYPE_CHECKING, Dict, FrozenSet, List, Set, Tuple, cast +from typing import TYPE_CHECKING, Dict, List, Set, Tuple, cast from .data import static_logic as static_witness_logic -from .data import utils from .data.item_definition_classes import DoorItemDefinition, ItemCategory, ProgressiveItemDefinition +from .data.utils import ( + WitnessRule, + define_new_region, + get_boat, + get_caves_except_path_to_challenge_exclusion_list, + get_complex_additional_panels, + get_complex_door_panels, + get_complex_doors, + get_disable_unrandomized_list, + get_discard_exclusion_list, + get_early_caves_list, + get_early_caves_start_list, + get_elevators_come_to_you, + get_ep_all_individual, + get_ep_easy, + get_ep_no_eclipse, + get_ep_obelisks, + get_laser_shuffle, + get_obelisk_keys, + get_simple_additional_panels, + get_simple_doors, + get_simple_panels, + get_symbol_shuffle_list, + get_vault_exclusion_list, + logical_and_witness_rules, + logical_or_witness_rules, + parse_lambda, +) if TYPE_CHECKING: from . import WitnessWorld @@ -32,8 +58,7 @@ class WitnessPlayerLogic: """WITNESS LOGIC CLASS""" - @lru_cache(maxsize=None) - def reduce_req_within_region(self, entity_hex: str) -> FrozenSet[FrozenSet[str]]: + def reduce_req_within_region(self, entity_hex: str) -> WitnessRule: """ Panels in this game often only turn on when other panels are solved. Those other panels may have different item requirements. @@ -42,35 +67,39 @@ def reduce_req_within_region(self, entity_hex: str) -> FrozenSet[FrozenSet[str]] Panels outside of the same region will still be checked manually. """ - if entity_hex in self.COMPLETELY_DISABLED_ENTITIES or entity_hex in self.IRRELEVANT_BUT_NOT_DISABLED_ENTITIES: + if self.is_disabled(entity_hex): return frozenset() entity_obj = self.REFERENCE_LOGIC.ENTITIES_BY_HEX[entity_hex] - these_items = frozenset({frozenset()}) + if entity_obj["region"] is not None and entity_obj["region"]["name"] in self.UNREACHABLE_REGIONS: + return frozenset() - if entity_obj["id"]: - these_items = self.DEPENDENT_REQUIREMENTS_BY_HEX[entity_hex]["items"] + # For the requirement of an entity, we consider two things: + # 1. Any items this entity needs (e.g. Symbols or Door Items) + these_items = self.DEPENDENT_REQUIREMENTS_BY_HEX[entity_hex].get("items", frozenset({frozenset()})) + # 2. Any entities that this entity depends on (e.g. one panel powering on the next panel in a set) + these_panels = self.DEPENDENT_REQUIREMENTS_BY_HEX[entity_hex]["entities"] + # Remove any items that don't actually exist in the settings (e.g. Symbol Shuffle turned off) these_items = frozenset({ subset.intersection(self.THEORETICAL_ITEMS_NO_MULTI) for subset in these_items }) + # Update the list of "items that are actually being used by any entity" for subset in these_items: self.PROG_ITEMS_ACTUALLY_IN_THE_GAME_NO_MULTI.update(subset) - these_panels = self.DEPENDENT_REQUIREMENTS_BY_HEX[entity_hex]["panels"] - + # If this entity is opened by a door item that exists in the itempool, add that item to its requirements. + # Also, remove any original power requirements this entity might have had. if entity_hex in self.DOOR_ITEMS_BY_ID: door_items = frozenset({frozenset([item]) for item in self.DOOR_ITEMS_BY_ID[entity_hex]}) - all_options: Set[FrozenSet[str]] = set() - for dependent_item in door_items: self.PROG_ITEMS_ACTUALLY_IN_THE_GAME_NO_MULTI.update(dependent_item) - for items_option in these_items: - all_options.add(items_option.union(dependent_item)) + + all_options = logical_and_witness_rules([door_items, these_items]) # If this entity is not an EP, and it has an associated door item, ignore the original power dependencies if static_witness_logic.ENTITIES_BY_HEX[entity_hex]["entityType"] != "EP": @@ -90,46 +119,70 @@ def reduce_req_within_region(self, entity_hex: str) -> FrozenSet[FrozenSet[str]] else: these_items = all_options - disabled_eps = {eHex for eHex in self.COMPLETELY_DISABLED_ENTITIES - if self.REFERENCE_LOGIC.ENTITIES_BY_HEX[eHex]["entityType"] == "EP"} - - these_panels = frozenset({panels - disabled_eps - for panels in these_panels}) - - if these_panels == frozenset({frozenset()}): - return these_items + # Now that we have item requirements and entity dependencies, it's time for the dependency reduction. - all_options = set() + # For each entity that this entity depends on (e.g. a panel turning on another panel), + # Add that entities requirements to this entity. + # If there are multiple options, consider each, and then or-chain them. + all_options = list() for option in these_panels: dependent_items_for_option = frozenset({frozenset()}) + # For each entity in this option, resolve it to its actual requirement. for option_entity in option: dep_obj = self.REFERENCE_LOGIC.ENTITIES_BY_HEX.get(option_entity) - if option_entity in self.ALWAYS_EVENT_NAMES_BY_HEX: - new_items = frozenset({frozenset([option_entity])}) - elif (entity_hex, option_entity) in self.CONDITIONAL_EVENTS: - new_items = frozenset({frozenset([option_entity])}) - self.USED_EVENT_NAMES_BY_HEX[option_entity] = self.CONDITIONAL_EVENTS[(entity_hex, option_entity)] - elif option_entity in {"7 Lasers", "11 Lasers", "7 Lasers + Redirect", "11 Lasers + Redirect", - "PP2 Weirdness", "Theater to Tunnels"}: + if option_entity in {"7 Lasers", "11 Lasers", "7 Lasers + Redirect", "11 Lasers + Redirect", + "PP2 Weirdness", "Theater to Tunnels"}: new_items = frozenset({frozenset([option_entity])}) + elif option_entity in self.DISABLE_EVERYTHING_BEHIND: + new_items = frozenset() else: - new_items = self.reduce_req_within_region(option_entity) - if dep_obj["region"] and entity_obj["region"] != dep_obj["region"]: - new_items = frozenset( - frozenset(possibility | {dep_obj["region"]["name"]}) - for possibility in new_items - ) + theoretical_new_items = self.get_entity_requirement(option_entity) + + if not theoretical_new_items: + # If the dependent entity is unsolvable & it is an EP, the current entity is an Obelisk Side. + # In this case, we actually have to skip it because it will just become pre-solved instead. + if dep_obj["entityType"] == "EP": + continue + # If the dependent entity is unsolvable and is NOT an EP, this requirement option is invalid. + new_items = frozenset() + elif option_entity in self.ALWAYS_EVENT_NAMES_BY_HEX: + new_items = frozenset({frozenset([option_entity])}) + elif (entity_hex, option_entity) in self.CONDITIONAL_EVENTS: + new_items = frozenset({frozenset([option_entity])}) + self.USED_EVENT_NAMES_BY_HEX[option_entity] = self.CONDITIONAL_EVENTS[ + (entity_hex, option_entity) + ] + else: + new_items = theoretical_new_items + if dep_obj["region"] and entity_obj["region"] != dep_obj["region"]: + new_items = frozenset( + frozenset(possibility | {dep_obj["region"]["name"]}) + for possibility in new_items + ) + + dependent_items_for_option = logical_and_witness_rules([dependent_items_for_option, new_items]) - dependent_items_for_option = utils.dnf_and([dependent_items_for_option, new_items]) + # Combine the resolved dependent entity requirements with the item requirements of this entity. + all_options.append(logical_and_witness_rules([these_items, dependent_items_for_option])) - for items_option in these_items: - for dependent_item in dependent_items_for_option: - all_options.add(items_option.union(dependent_item)) + # or-chain all separate dependent entity options. + return logical_or_witness_rules(all_options) - return utils.dnf_remove_redundancies(frozenset(all_options)) + def get_entity_requirement(self, entity_hex: str) -> WitnessRule: + """ + Get requirement of entity by its hex code. + These requirements are cached, with the actual function calculating them being reduce_req_within_region. + """ + requirement = self.REQUIREMENTS_BY_HEX.get(entity_hex) + + if requirement is None: + requirement = self.reduce_req_within_region(entity_hex) + self.REQUIREMENTS_BY_HEX[entity_hex] = requirement + + return requirement def make_single_adjustment(self, adj_type: str, line: str) -> None: from .data import static_items as static_witness_items @@ -191,11 +244,11 @@ def make_single_adjustment(self, adj_type: str, line: str) -> None: line_split = line.split(" - ") requirement = { - "panels": utils.parse_lambda(line_split[1]), + "entities": parse_lambda(line_split[1]), } if len(line_split) > 2: - required_items = utils.parse_lambda(line_split[2]) + required_items = parse_lambda(line_split[2]) items_actually_in_the_game = [ item_name for item_name, item_definition in static_witness_logic.ALL_ITEMS.items() if item_definition.category is ItemCategory.SYMBOL @@ -226,9 +279,9 @@ def make_single_adjustment(self, adj_type: str, line: str) -> None: return if adj_type == "Region Changes": - new_region_and_options = utils.define_new_region(line + ":") + new_region_and_options = define_new_region(line + ":") - self.CONNECTIONS_BY_REGION_NAME[new_region_and_options[0]["name"]] = new_region_and_options[1] + self.CONNECTIONS_BY_REGION_NAME_THEORETICAL[new_region_and_options[0]["name"]] = new_region_and_options[1] return @@ -238,102 +291,99 @@ def make_single_adjustment(self, adj_type: str, line: str) -> None: target_region = line_split[1] panel_set_string = line_split[2] - for connection in self.CONNECTIONS_BY_REGION_NAME[source_region]: + for connection in self.CONNECTIONS_BY_REGION_NAME_THEORETICAL[source_region]: if connection[0] == target_region: - self.CONNECTIONS_BY_REGION_NAME[source_region].remove(connection) + self.CONNECTIONS_BY_REGION_NAME_THEORETICAL[source_region].remove(connection) if panel_set_string == "TrueOneWay": - self.CONNECTIONS_BY_REGION_NAME[source_region].add( + self.CONNECTIONS_BY_REGION_NAME_THEORETICAL[source_region].add( (target_region, frozenset({frozenset(["TrueOneWay"])})) ) else: - new_lambda = connection[1] | utils.parse_lambda(panel_set_string) - self.CONNECTIONS_BY_REGION_NAME[source_region].add((target_region, new_lambda)) + new_lambda = logical_or_witness_rules([connection[1], parse_lambda(panel_set_string)]) + self.CONNECTIONS_BY_REGION_NAME_THEORETICAL[source_region].add((target_region, new_lambda)) break - else: # Execute if loop did not break. TIL this is a thing you can do! - new_conn = (target_region, utils.parse_lambda(panel_set_string)) - self.CONNECTIONS_BY_REGION_NAME[source_region].add(new_conn) + else: + new_conn = (target_region, parse_lambda(panel_set_string)) + self.CONNECTIONS_BY_REGION_NAME_THEORETICAL[source_region].add(new_conn) if adj_type == "Added Locations": if "0x" in line: line = self.REFERENCE_LOGIC.ENTITIES_BY_HEX[line]["checkName"] self.ADDED_CHECKS.add(line) - @staticmethod - def handle_postgame(world: "WitnessWorld") -> List[List[str]]: - # In shuffle_postgame, panels that become accessible "after or at the same time as the goal" are disabled. - # This has a lot of complicated considerations, which I'll try my best to explain. + def handle_postgame(self, world: "WitnessWorld") -> List[List[str]]: + """ + In shuffle_postgame, panels that become accessible "after or at the same time as the goal" are disabled. + This mostly involves the disabling of key panels (e.g. long box when the goal is short box). + These will then hava a cascading effect on other entities that are locked "behind" them. + """ + postgame_adjustments = [] # Make some quick references to some options - doors = world.options.shuffle_doors >= 2 # "Panels" mode has no overarching region accessibility implications. + remote_doors = world.options.shuffle_doors >= 2 # "Panels" mode has no region accessibility implications. early_caves = world.options.early_caves victory = world.options.victory_condition mnt_lasers = world.options.mountain_lasers chal_lasers = world.options.challenge_lasers - # Goal is "short box" but short box requires more lasers than long box - reverse_shortbox_goal = victory == "mountain_box_short" and mnt_lasers > chal_lasers - # Goal is "short box", and long box requires at least as many lasers as short box (as god intended) proper_shortbox_goal = victory == "mountain_box_short" and chal_lasers >= mnt_lasers # Goal is "long box", but short box requires at least as many lasers than long box. reverse_longbox_goal = victory == "mountain_box_long" and mnt_lasers >= chal_lasers - # If goal is shortbox or "reverse longbox", you will never enter the mountain from the top before winning. - mountain_enterable_from_top = not (victory == "mountain_box_short" or reverse_longbox_goal) + # ||| Section 1: Proper postgame cases ||| + # When something only comes into logic after the goal, e.g. "longbox is postgame if the goal is shortbox". - # Caves & Challenge should never have anything if doors are vanilla - definitionally "post-game" - # This is technically imprecise, but it matches player expectations better. - if not (early_caves or doors): - postgame_adjustments.append(utils.get_caves_exclusion_list()) - postgame_adjustments.append(utils.get_beyond_challenge_exclusion_list()) + # Disable anything directly locked by the victory panel + self.DISABLE_EVERYTHING_BEHIND.add(self.VICTORY_LOCATION) - # If Challenge is the goal, some panels on the way need to be left on, as well as Challenge Vault box itself - if not victory == "challenge": - postgame_adjustments.append(utils.get_path_to_challenge_exclusion_list()) - postgame_adjustments.append(utils.get_challenge_vault_box_exclusion_list()) - - # Challenge can only have something if the goal is not challenge or longbox itself. - # In case of shortbox, it'd have to be a "reverse shortbox" situation where shortbox requires *more* lasers. - # In that case, it'd also have to be a doors mode, but that's already covered by the previous block. - if not (victory == "elevator" or reverse_shortbox_goal): - postgame_adjustments.append(utils.get_beyond_challenge_exclusion_list()) - if not victory == "challenge": - postgame_adjustments.append(utils.get_challenge_vault_box_exclusion_list()) - - # Mountain can't be reached if the goal is shortbox (or "reverse long box") - if not mountain_enterable_from_top: - postgame_adjustments.append(utils.get_mountain_upper_exclusion_list()) - - # Same goes for lower mountain, but that one *can* be reached in remote doors modes. - if not doors: - postgame_adjustments.append(utils.get_mountain_lower_exclusion_list()) - - # The Mountain Bottom Floor Discard is a bit complicated, so we handle it separately. ("it" == the Discard) - # In Elevator Goal, it is definitionally in the post-game, unless remote doors is played. - # In Challenge Goal, it is before the Challenge, so it is not post-game. - # In Short Box Goal, you can win before turning it on, UNLESS Short Box requires MORE lasers than long box. - # In Long Box Goal, it is always in the post-game because solving long box is what turns it on. - if not ((victory == "elevator" and doors) or victory == "challenge" or (reverse_shortbox_goal and doors)): - # We now know Bottom Floor Discard is in the post-game. - # This has different consequences depending on whether remote doors is being played. - # If doors are vanilla, Bottom Floor Discard locks a door to an area, which has to be disabled as well. - if doors: - postgame_adjustments.append(utils.get_bottom_floor_discard_exclusion_list()) - else: - postgame_adjustments.append(utils.get_bottom_floor_discard_nondoors_exclusion_list()) - - # In Challenge goal + early_caves + vanilla doors, you could find something important on Bottom Floor Discard, - # including the Caves Shortcuts themselves if playing "early_caves: start_inventory". - # This is another thing that was deemed "unfun" more than fitting the actual definition of post-game. - if victory == "challenge" and early_caves and not doors: - postgame_adjustments.append(utils.get_bottom_floor_discard_nondoors_exclusion_list()) + # If we have a long box goal, Challenge is behind the amount of lasers required to just win. + # This is technically slightly incorrect as the Challenge Vault Box could contain a *symbol* that is required + # to open Mountain Entry (Stars 2). However, since there is a very easy sphere 1 snipe, this is not considered. + if victory == "mountain_box_long": + postgame_adjustments.append(["Disabled Locations:", "0x0A332 (Challenge Timer Start)"]) - # If we have a proper short box goal, long box will never be activated first. + # If we have a proper short box goal, anything based on challenge lasers will never have something required. if proper_shortbox_goal: postgame_adjustments.append(["Disabled Locations:", "0xFFF00 (Mountain Box Long)"]) + postgame_adjustments.append(["Disabled Locations:", "0x0A332 (Challenge Timer Start)"]) + + # In a case where long box can be activated before short box, short box is postgame. + if reverse_longbox_goal: + postgame_adjustments.append(["Disabled Locations:", "0x09F7F (Mountain Box Short)"]) + + # ||| Section 2: "Fun" considerations ||| + # These are cases in which it was deemed "unfun" to have an "oops, all lasers" situation, especially when + # it's for a single possible item. + + mbfd_extra_exclusions = ( + # Progressive Dots 2 behind 11 lasers in an Elevator seed with vanilla doors = :( + victory == "elevator" and not remote_doors + + # Caves Shortcuts / Challenge Entry (Panel) on MBFD in a Challenge seed with vanilla doors = :( + or victory == "challenge" and early_caves and not remote_doors + ) + + if mbfd_extra_exclusions: + postgame_adjustments.append(["Disabled Locations:", "0xFFF00 (Mountain Box Long)"]) + + # Another big postgame case that is missed is "Desert Laser Redirect (Panel)". + # An 11 lasers longbox seed could technically have this item on Challenge Vault Box. + # This case is not considered and we will act like Desert Laser Redirect (Panel) is always accessible. + # (Which means we do no additional work, this comment just exists to document that case) + + # ||| Section 3: "Post-or-equal-game" cases ||| + # These are cases in which something comes into logic *at the same time* as your goal and thus also can't + # possibly have a required item. These can be a bit awkward. + + # When your victory is Challenge, but you have to get to it the vanilla way, there are no required items + # that can show up in the Caves that aren't also needed on the descent through Mountain. + # So, we should disable all entities in the Caves and Tunnels *except* for those that are required to enter. + if not (early_caves or remote_doors) and victory == "challenge": + postgame_adjustments.append(get_caves_except_path_to_challenge_exclusion_list()) return postgame_adjustments @@ -343,7 +393,7 @@ def make_options_adjustments(self, world: "WitnessWorld") -> None: # Make condensed references to some options - doors = world.options.shuffle_doors >= 2 # "Panels" mode has no overarching region accessibility implications. + remote_doors = world.options.shuffle_doors >= 2 # "Panels" mode has no overarching region access implications. lasers = world.options.shuffle_lasers victory = world.options.victory_condition mnt_lasers = world.options.mountain_lasers @@ -357,16 +407,16 @@ def make_options_adjustments(self, world: "WitnessWorld") -> None: if not world.options.shuffle_discarded_panels: # In disable_non_randomized, the discards are needed for alternate activation triggers, UNLESS both # (remote) doors and lasers are shuffled. - if not world.options.disable_non_randomized_puzzles or (doors and lasers): - adjustment_linesets_in_order.append(utils.get_discard_exclusion_list()) + if not world.options.disable_non_randomized_puzzles or (remote_doors and lasers): + adjustment_linesets_in_order.append(get_discard_exclusion_list()) - if doors: - adjustment_linesets_in_order.append(utils.get_bottom_floor_discard_exclusion_list()) + if remote_doors: + adjustment_linesets_in_order.append(["Disabled Locations:", "0x17FA2"]) if not world.options.shuffle_vault_boxes: - adjustment_linesets_in_order.append(utils.get_vault_exclusion_list()) + adjustment_linesets_in_order.append(get_vault_exclusion_list()) if not victory == "challenge": - adjustment_linesets_in_order.append(utils.get_challenge_vault_box_exclusion_list()) + adjustment_linesets_in_order.append(["Disabled Locations:", "0x0A332"]) # Victory Condition @@ -389,54 +439,54 @@ def make_options_adjustments(self, world: "WitnessWorld") -> None: ]) if world.options.disable_non_randomized_puzzles: - adjustment_linesets_in_order.append(utils.get_disable_unrandomized_list()) + adjustment_linesets_in_order.append(get_disable_unrandomized_list()) if world.options.shuffle_symbols: - adjustment_linesets_in_order.append(utils.get_symbol_shuffle_list()) + adjustment_linesets_in_order.append(get_symbol_shuffle_list()) if world.options.EP_difficulty == "normal": - adjustment_linesets_in_order.append(utils.get_ep_easy()) + adjustment_linesets_in_order.append(get_ep_easy()) elif world.options.EP_difficulty == "tedious": - adjustment_linesets_in_order.append(utils.get_ep_no_eclipse()) + adjustment_linesets_in_order.append(get_ep_no_eclipse()) if world.options.door_groupings == "regional": if world.options.shuffle_doors == "panels": - adjustment_linesets_in_order.append(utils.get_simple_panels()) + adjustment_linesets_in_order.append(get_simple_panels()) elif world.options.shuffle_doors == "doors": - adjustment_linesets_in_order.append(utils.get_simple_doors()) + adjustment_linesets_in_order.append(get_simple_doors()) elif world.options.shuffle_doors == "mixed": - adjustment_linesets_in_order.append(utils.get_simple_doors()) - adjustment_linesets_in_order.append(utils.get_simple_additional_panels()) + adjustment_linesets_in_order.append(get_simple_doors()) + adjustment_linesets_in_order.append(get_simple_additional_panels()) else: if world.options.shuffle_doors == "panels": - adjustment_linesets_in_order.append(utils.get_complex_door_panels()) - adjustment_linesets_in_order.append(utils.get_complex_additional_panels()) + adjustment_linesets_in_order.append(get_complex_door_panels()) + adjustment_linesets_in_order.append(get_complex_additional_panels()) elif world.options.shuffle_doors == "doors": - adjustment_linesets_in_order.append(utils.get_complex_doors()) + adjustment_linesets_in_order.append(get_complex_doors()) elif world.options.shuffle_doors == "mixed": - adjustment_linesets_in_order.append(utils.get_complex_doors()) - adjustment_linesets_in_order.append(utils.get_complex_additional_panels()) + adjustment_linesets_in_order.append(get_complex_doors()) + adjustment_linesets_in_order.append(get_complex_additional_panels()) if world.options.shuffle_boat: - adjustment_linesets_in_order.append(utils.get_boat()) + adjustment_linesets_in_order.append(get_boat()) if world.options.early_caves == "starting_inventory": - adjustment_linesets_in_order.append(utils.get_early_caves_start_list()) + adjustment_linesets_in_order.append(get_early_caves_start_list()) - if world.options.early_caves == "add_to_pool" and not doors: - adjustment_linesets_in_order.append(utils.get_early_caves_list()) + if world.options.early_caves == "add_to_pool" and not remote_doors: + adjustment_linesets_in_order.append(get_early_caves_list()) if world.options.elevators_come_to_you: - adjustment_linesets_in_order.append(utils.get_elevators_come_to_you()) + adjustment_linesets_in_order.append(get_elevators_come_to_you()) for item in self.YAML_ADDED_ITEMS: adjustment_linesets_in_order.append(["Items:", item]) if lasers: - adjustment_linesets_in_order.append(utils.get_laser_shuffle()) + adjustment_linesets_in_order.append(get_laser_shuffle()) if world.options.shuffle_EPs and world.options.obelisk_keys: - adjustment_linesets_in_order.append(utils.get_obelisk_keys()) + adjustment_linesets_in_order.append(get_obelisk_keys()) if world.options.shuffle_EPs == "obelisk_sides": ep_gen = ((ep_hex, ep_obj) for (ep_hex, ep_obj) in self.REFERENCE_LOGIC.ENTITIES_BY_HEX.items() @@ -448,10 +498,10 @@ def make_options_adjustments(self, world: "WitnessWorld") -> None: ep_name = self.REFERENCE_LOGIC.ENTITIES_BY_HEX[ep_hex]["checkName"] self.ALWAYS_EVENT_NAMES_BY_HEX[ep_hex] = f"{obelisk_name} - {ep_name}" else: - adjustment_linesets_in_order.append(["Disabled Locations:"] + utils.get_ep_obelisks()[1:]) + adjustment_linesets_in_order.append(["Disabled Locations:"] + get_ep_obelisks()[1:]) if not world.options.shuffle_EPs: - adjustment_linesets_in_order.append(["Disabled Locations:"] + utils.get_ep_all_individual()[1:]) + adjustment_linesets_in_order.append(["Disabled Locations:"] + get_ep_all_individual()[1:]) for yaml_disabled_location in self.YAML_DISABLED_LOCATIONS: if yaml_disabled_location not in self.REFERENCE_LOGIC.ENTITIES_BY_NAME: @@ -482,16 +532,189 @@ def make_options_adjustments(self, world: "WitnessWorld") -> None: if entity_id in self.DOOR_ITEMS_BY_ID: del self.DOOR_ITEMS_BY_ID[entity_id] - def make_dependency_reduced_checklist(self) -> None: + def discover_reachable_regions(self): + """ + Some options disable panels or remove specific items. + This can make entire regions completely unreachable, because all their incoming connections are invalid. + This function starts from the Entry region and performs a graph search to discover all reachable regions. + """ + reachable_regions = {"Entry"} + new_regions_found = True + + # This for loop "floods" the region graph until no more new regions are discovered. + # Note that connections that rely on disabled entities are considered invalid. + # This fact may lead to unreachable regions being discovered. + while new_regions_found: + new_regions_found = False + regions_to_check = reachable_regions.copy() + + # Find new regions through connections from currently reachable regions + while regions_to_check: + next_region = regions_to_check.pop() + + for region_exit in self.CONNECTIONS_BY_REGION_NAME[next_region]: + target = region_exit[0] + + if target in reachable_regions: + continue + + # There may be multiple conncetions between two regions. We should check all of them to see if + # any of them are valid. + for option in region_exit[1]: + # If a connection requires having access to a not-yet-reached region, do not consider it. + # Otherwise, this connection is valid, and the target region is reachable -> break for loop + if not any(req in self.CONNECTIONS_BY_REGION_NAME and req not in reachable_regions + for req in option): + break + # If none of the connections were valid, this region is not reachable this way, for now. + else: + continue + + new_regions_found = True + regions_to_check.add(target) + reachable_regions.add(target) + + return reachable_regions + + def find_unsolvable_entities(self, world: "WitnessWorld") -> None: + """ + Settings like "shuffle_postgame: False" may disable certain panels. + This may make panels or regions logically locked by those panels unreachable. + We will determine these automatically and disable them as well. + """ + + all_regions = set(self.CONNECTIONS_BY_REGION_NAME_THEORETICAL) + + while True: + # Re-make the dependency reduced entity requirements dict, which depends on currently + self.make_dependency_reduced_checklist() + + # Check if any regions have become unreachable. + reachable_regions = self.discover_reachable_regions() + new_unreachable_regions = all_regions - reachable_regions - self.UNREACHABLE_REGIONS + if new_unreachable_regions: + self.UNREACHABLE_REGIONS.update(new_unreachable_regions) + + # Then, discover unreachable entities. + newly_discovered_disabled_entities = set() + + # First, entities in unreachable regions are obviously themselves unreachable. + for region in new_unreachable_regions: + for entity in static_witness_logic.ALL_REGIONS_BY_NAME[region]["physical_entities"]: + # Never disable the Victory Location. + if entity == self.VICTORY_LOCATION: + continue + + # Never disable a laser (They should still function even if you can't walk up to them). + if static_witness_logic.ENTITIES_BY_HEX[entity]["entityType"] == "Laser": + continue + + newly_discovered_disabled_entities.add(entity) + + # Secondly, any entities that depend on disabled entities are unreachable as well. + for entity, req in self.REQUIREMENTS_BY_HEX.items(): + # If the requirement is empty (unsolvable) and it isn't disabled already, add it to "newly disabled" + if not req and not self.is_disabled(entity): + # Never disable the Victory Location. + if entity == self.VICTORY_LOCATION: + continue + + # If we are disabling a laser, something has gone wrong. + if static_witness_logic.ENTITIES_BY_HEX[entity]["entityType"] == "Laser": + laser_name = static_witness_logic.ENTITIES_BY_HEX[entity]["checkName"] + player_name = world.multiworld.get_player_name(world.player) + raise RuntimeError(f"Somehow, {laser_name} was disabled for player {player_name}." + f" This is not allowed to happen, please report to Violet.") + + newly_discovered_disabled_entities.add(entity) + + # Disable the newly determined unreachable entities. + self.COMPLETELY_DISABLED_ENTITIES.update(newly_discovered_disabled_entities) + + # If we didn't find any new unreachable regions or entities this cycle, we are done. + # If we did, we need to do another cycle to see if even more regions or entities became unreachable. + if not new_unreachable_regions and not newly_discovered_disabled_entities: + return + + def reduce_connection_requirement(self, connection: Tuple[str, WitnessRule]) -> WitnessRule: + all_possibilities = [] + + # Check each traversal option individually + for option in connection[1]: + individual_entity_requirements = [] + for entity in option: + # If a connection requires solving a disabled entity, it is not valid. + if not self.solvability_guaranteed(entity) or entity in self.DISABLE_EVERYTHING_BEHIND: + individual_entity_requirements.append(frozenset()) + # If a connection requires acquiring an event, add that event to its requirements. + elif (entity in self.ALWAYS_EVENT_NAMES_BY_HEX + or entity not in self.REFERENCE_LOGIC.ENTITIES_BY_HEX): + individual_entity_requirements.append(frozenset({frozenset({entity})})) + # If a connection requires entities, use their newly calculated independent requirements. + else: + entity_req = self.get_entity_requirement(entity) + + if self.REFERENCE_LOGIC.ENTITIES_BY_HEX[entity]["region"]: + region_name = self.REFERENCE_LOGIC.ENTITIES_BY_HEX[entity]["region"]["name"] + entity_req = logical_and_witness_rules([entity_req, frozenset({frozenset({region_name})})]) + + individual_entity_requirements.append(entity_req) + + # Merge all possible requirements into one DNF condition. + all_possibilities.append(logical_and_witness_rules(individual_entity_requirements)) + + return logical_or_witness_rules(all_possibilities) + + def make_dependency_reduced_checklist(self): """ - Turns dependent check set into semi-independent check set + Every entity has a requirement. This requirement may involve other entities. + Example: Solving a panel powers a cable, and that cable turns on the next panel. + These dependencies are specified in the logic files (e.g. "WitnessLogic.txt") and may be modified by options. + + Recursively having to check the requirements of every dependent entity would be very slow, so we go through this + recursion once and make a single, independent requirement for each entity. + + This requirement may include symbol items, door items, regions, or events. + A requirement is saved as a two-dimensional set that represents a disjuntive normal form. """ + # Requirements are cached per entity. However, we might redo the whole reduction process multiple times. + # So, we first clear this cache. + self.REQUIREMENTS_BY_HEX = dict() + + # We also clear any data structures that we might have filled in a previous dependency reduction + self.REQUIREMENTS_BY_HEX = dict() + self.USED_EVENT_NAMES_BY_HEX = dict() + self.CONNECTIONS_BY_REGION_NAME = dict() + self.PROG_ITEMS_ACTUALLY_IN_THE_GAME_NO_MULTI = set() + + # Make independent requirements for entities for entity_hex in self.DEPENDENT_REQUIREMENTS_BY_HEX.keys(): - indep_requirement = self.reduce_req_within_region(entity_hex) + indep_requirement = self.get_entity_requirement(entity_hex) self.REQUIREMENTS_BY_HEX[entity_hex] = indep_requirement + # Make independent region connection requirements based on the entities they require + for region, connections in self.CONNECTIONS_BY_REGION_NAME_THEORETICAL.items(): + self.CONNECTIONS_BY_REGION_NAME[region] = [] + + new_connections = [] + + for connection in connections: + overall_requirement = self.reduce_connection_requirement(connection) + + # If there is a way to use this connection, add it. + if overall_requirement: + new_connections.append((connection[0], overall_requirement)) + + # If there are any usable outgoing connections from this region, add them. + if new_connections: + self.CONNECTIONS_BY_REGION_NAME[region] = new_connections + + def finalize_items(self): + """ + Finalise which items are used in the world, and handle their progressive versions. + """ for item in self.PROG_ITEMS_ACTUALLY_IN_THE_GAME_NO_MULTI: if item not in self.THEORETICAL_ITEMS: progressive_item_name = static_witness_logic.get_parent_progressive_item(item) @@ -505,33 +728,6 @@ def make_dependency_reduced_checklist(self) -> None: else: self.PROG_ITEMS_ACTUALLY_IN_THE_GAME.add(item) - for region, connections in self.CONNECTIONS_BY_REGION_NAME.items(): - new_connections = [] - - for connection in connections: - overall_requirement = frozenset() - - for option in connection[1]: - individual_entity_requirements = [] - for entity in option: - if (entity in self.ALWAYS_EVENT_NAMES_BY_HEX - or entity not in self.REFERENCE_LOGIC.ENTITIES_BY_HEX): - individual_entity_requirements.append(frozenset({frozenset({entity})})) - else: - entity_req = self.reduce_req_within_region(entity) - - if self.REFERENCE_LOGIC.ENTITIES_BY_HEX[entity]["region"]: - region_name = self.REFERENCE_LOGIC.ENTITIES_BY_HEX[entity]["region"]["name"] - entity_req = utils.dnf_and([entity_req, frozenset({frozenset({region_name})})]) - - individual_entity_requirements.append(entity_req) - - overall_requirement |= utils.dnf_and(individual_entity_requirements) - - new_connections.append((connection[0], overall_requirement)) - - self.CONNECTIONS_BY_REGION_NAME[region] = new_connections - def solvability_guaranteed(self, entity_hex: str) -> bool: return not ( entity_hex in self.ENTITIES_WITHOUT_ENSURED_SOLVABILITY @@ -539,6 +735,12 @@ def solvability_guaranteed(self, entity_hex: str) -> bool: or entity_hex in self.IRRELEVANT_BUT_NOT_DISABLED_ENTITIES ) + def is_disabled(self, entity_hex: str) -> bool: + return ( + entity_hex in self.COMPLETELY_DISABLED_ENTITIES + or entity_hex in self.IRRELEVANT_BUT_NOT_DISABLED_ENTITIES + ) + def determine_unrequired_entities(self, world: "WitnessWorld") -> None: """Figure out which major items are actually useless in this world's settings""" @@ -588,7 +790,6 @@ def determine_unrequired_entities(self, world: "WitnessWorld") -> None: "0x01BEA": difficulty == "none" and eps_shuffled, # Keep PP2 "0x0A0C9": eps_shuffled or discards_shuffled or disable_non_randomized, # Cargo Box Entry Door "0x09EEB": discards_shuffled or mountain_upper_included, # Mountain Floor 2 Elevator Control Panel - "0x09EDD": mountain_upper_included, # Mountain Floor 2 Exit Door "0x17CAB": symbols_shuffled or not disable_non_randomized or "0x17CAB" not in self.DOOR_ITEMS_BY_ID, # Jungle Popup Wall Panel } @@ -598,20 +799,24 @@ def determine_unrequired_entities(self, world: "WitnessWorld") -> None: item_name for item_name, is_required in is_item_required_dict.items() if not is_required } - def make_event_item_pair(self, panel: str) -> Tuple[str, str]: + def make_event_item_pair(self, entity_hex: str) -> Tuple[str, str]: """ Makes a pair of an event panel and its event item """ - action = " Opened" if self.REFERENCE_LOGIC.ENTITIES_BY_HEX[panel]["entityType"] == "Door" else " Solved" + action = " Opened" if self.REFERENCE_LOGIC.ENTITIES_BY_HEX[entity_hex]["entityType"] == "Door" else " Solved" - name = self.REFERENCE_LOGIC.ENTITIES_BY_HEX[panel]["checkName"] + action - if panel not in self.USED_EVENT_NAMES_BY_HEX: - warning(f'Panel "{name}" does not have an associated event name.') - self.USED_EVENT_NAMES_BY_HEX[panel] = name + " Event" - pair = (name, self.USED_EVENT_NAMES_BY_HEX[panel]) + name = self.REFERENCE_LOGIC.ENTITIES_BY_HEX[entity_hex]["checkName"] + action + if entity_hex not in self.USED_EVENT_NAMES_BY_HEX: + warning(f'Entity "{name}" does not have an associated event name.') + self.USED_EVENT_NAMES_BY_HEX[entity_hex] = name + " Event" + pair = (name, self.USED_EVENT_NAMES_BY_HEX[entity_hex]) return pair def make_event_panel_lists(self) -> None: + """ + Makes event-item pairs for entities with associated events, unless these entities are disabled. + """ + self.ALWAYS_EVENT_NAMES_BY_HEX[self.VICTORY_LOCATION] = "Victory" self.USED_EVENT_NAMES_BY_HEX.update(self.ALWAYS_EVENT_NAMES_BY_HEX) @@ -636,6 +841,8 @@ def __init__(self, world: "WitnessWorld", disabled_locations: Set[str], start_in self.ENTITIES_WITHOUT_ENSURED_SOLVABILITY = set() + self.UNREACHABLE_REGIONS = set() + self.THEORETICAL_ITEMS = set() self.THEORETICAL_ITEMS_NO_MULTI = set() self.MULTI_AMOUNTS = defaultdict(lambda: 1) @@ -654,14 +861,16 @@ def __init__(self, world: "WitnessWorld", disabled_locations: Set[str], start_in elif self.DIFFICULTY == "none": self.REFERENCE_LOGIC = static_witness_logic.vanilla - self.CONNECTIONS_BY_REGION_NAME = copy.deepcopy(self.REFERENCE_LOGIC.STATIC_CONNECTIONS_BY_REGION_NAME) + self.CONNECTIONS_BY_REGION_NAME_THEORETICAL = copy.deepcopy( + self.REFERENCE_LOGIC.STATIC_CONNECTIONS_BY_REGION_NAME + ) + self.CONNECTIONS_BY_REGION_NAME = dict() self.DEPENDENT_REQUIREMENTS_BY_HEX = copy.deepcopy(self.REFERENCE_LOGIC.STATIC_DEPENDENT_REQUIREMENTS_BY_HEX) self.REQUIREMENTS_BY_HEX = dict() - # Determining which panels need to be events is a difficult process. - # At the end, we will have EVENT_ITEM_PAIRS for all the necessary ones. self.EVENT_ITEM_PAIRS = dict() self.COMPLETELY_DISABLED_ENTITIES = set() + self.DISABLE_EVERYTHING_BEHIND = set() self.PRECOMPLETED_LOCATIONS = set() self.EXCLUDED_LOCATIONS = set() self.ADDED_CHECKS = set() @@ -687,7 +896,18 @@ def __init__(self, world: "WitnessWorld", disabled_locations: Set[str], start_in self.USED_EVENT_NAMES_BY_HEX = {} self.CONDITIONAL_EVENTS = {} + # The basic requirements to solve each entity come from StaticWitnessLogic. + # However, for any given world, the options (e.g. which item shuffles are enabled) affect the requirements. self.make_options_adjustments(world) self.determine_unrequired_entities(world) + self.find_unsolvable_entities(world) + + # After we have adjusted the raw requirements, we perform a dependency reduction for the entity requirements. + # This will make the access conditions way faster, instead of recursively checking dependent entities each time. self.make_dependency_reduced_checklist() + + # Finalize which items actually exist in the MultiWorld and which get grouped into progressive items. + self.finalize_items() + + # Create event-item pairs for specific panels in the game. self.make_event_panel_lists() diff --git a/worlds/witness/regions.py b/worlds/witness/regions.py index e1f0ddb2161f..35f4e9544212 100644 --- a/worlds/witness/regions.py +++ b/worlds/witness/regions.py @@ -3,13 +3,14 @@ and connects them with the proper requirements """ from collections import defaultdict -from typing import TYPE_CHECKING, Dict, FrozenSet, List, Tuple +from typing import TYPE_CHECKING, Dict, List, Set, Tuple from BaseClasses import Entrance, Region from worlds.generic.Rules import CollectionRule from .data import static_logic as static_witness_logic +from .data.utils import WitnessRule, optimize_witness_rule from .locations import WitnessPlayerLocations, static_witness_locations from .player_logic import WitnessPlayerLogic @@ -24,7 +25,7 @@ class WitnessPlayerRegions: logic = None @staticmethod - def make_lambda(item_requirement: FrozenSet[FrozenSet[str]], world: "WitnessWorld") -> CollectionRule: + def make_lambda(item_requirement: WitnessRule, world: "WitnessWorld") -> CollectionRule: from .rules import _meets_item_requirements """ @@ -34,8 +35,8 @@ def make_lambda(item_requirement: FrozenSet[FrozenSet[str]], world: "WitnessWorl return _meets_item_requirements(item_requirement, world) - def connect_if_possible(self, world: "WitnessWorld", source: str, target: str, req: FrozenSet[FrozenSet[str]], - regions_by_name: Dict[str, Region], backwards: bool = False): + def connect_if_possible(self, world: "WitnessWorld", source: str, target: str, req: WitnessRule, + regions_by_name: Dict[str, Region]): """ connect two regions and set the corresponding requirement """ @@ -43,10 +44,6 @@ def connect_if_possible(self, world: "WitnessWorld", source: str, target: str, r # Remove any possibilities where being in the target region would be required anyway. real_requirement = frozenset({option for option in req if target not in option}) - # There are some connections that should only be done one way. If this is a backwards connection, check for that - if backwards: - real_requirement = frozenset({option for option in real_requirement if "TrueOneWay" not in option}) - # Dissolve any "True" or "TrueOneWay" real_requirement = frozenset({option - {"True", "TrueOneWay"} for option in real_requirement}) @@ -56,12 +53,12 @@ def connect_if_possible(self, world: "WitnessWorld", source: str, target: str, r # We don't need to check for the accessibility of the source region. final_requirement = frozenset({option - frozenset({source}) for option in real_requirement}) + final_requirement = optimize_witness_rule(final_requirement) source_region = regions_by_name[source] target_region = regions_by_name[target] - backwards = " Backwards" if backwards else "" - connection_name = source + " to " + target + backwards + connection_name = source + " to " + target connection = Entrance( world.player, @@ -74,7 +71,8 @@ def connect_if_possible(self, world: "WitnessWorld", source: str, target: str, r source_region.exits.append(connection) connection.connect(target_region) - self.created_entrances[source, target].append(connection) + self.two_way_entrance_register[source, target].append(connection) + self.two_way_entrance_register[target, source].append(connection) # Register any necessary indirect connections mentioned_regions = { @@ -94,14 +92,19 @@ def create_regions(self, world: "WitnessWorld", player_logic: WitnessPlayerLogic all_locations = set() regions_by_name = dict() - for region_name, region in self.reference_logic.ALL_REGIONS_BY_NAME.items(): + regions_to_create = { + k: v for k, v in self.reference_logic.ALL_REGIONS_BY_NAME.items() + if k not in player_logic.UNREACHABLE_REGIONS + } + + for region_name, region in regions_to_create.items(): locations_for_this_region = [ - self.reference_logic.ENTITIES_BY_HEX[panel]["checkName"] for panel in region["panels"] + self.reference_logic.ENTITIES_BY_HEX[panel]["checkName"] for panel in region["entities"] if self.reference_logic.ENTITIES_BY_HEX[panel]["checkName"] in self.player_locations.CHECK_LOCATION_TABLE ] locations_for_this_region += [ - static_witness_locations.get_event_name(panel) for panel in region["panels"] + static_witness_locations.get_event_name(panel) for panel in region["entities"] if static_witness_locations.get_event_name(panel) in self.player_locations.EVENT_LOCATION_TABLE ] @@ -111,31 +114,13 @@ def create_regions(self, world: "WitnessWorld", player_logic: WitnessPlayerLogic regions_by_name[region_name] = new_region - for region_name, region in self.reference_logic.ALL_REGIONS_BY_NAME.items(): - for connection in player_logic.CONNECTIONS_BY_REGION_NAME[region_name]: - self.connect_if_possible(world, region_name, connection[0], connection[1], regions_by_name) - self.connect_if_possible(world, connection[0], region_name, connection[1], regions_by_name, True) - - # find regions that are completely disconnected from the start node and remove them - regions_to_check = {"Menu"} - reachable_regions = {"Menu"} + self.created_region_names = set(regions_by_name) - while regions_to_check: - next_region = regions_to_check.pop() - region_obj = regions_by_name[next_region] + world.multiworld.regions += regions_by_name.values() - for exit in region_obj.exits: - target = exit.connected_region - - if target.name in reachable_regions: - continue - - regions_to_check.add(target.name) - reachable_regions.add(target.name) - - self.created_regions = {k: v for k, v in regions_by_name.items() if k in reachable_regions} - - world.multiworld.regions += self.created_regions.values() + for region_name, region in regions_to_create.items(): + for connection in player_logic.CONNECTIONS_BY_REGION_NAME[region_name]: + self.connect_if_possible(world, region_name, connection[0], connection[1], regions_by_name) def __init__(self, player_locations: WitnessPlayerLocations, world: "WitnessWorld") -> None: difficulty = world.options.puzzle_randomization @@ -148,5 +133,5 @@ def __init__(self, player_locations: WitnessPlayerLocations, world: "WitnessWorl self.reference_logic = static_witness_logic.vanilla self.player_locations = player_locations - self.created_entrances: Dict[Tuple[str, str], List[Entrance]] = defaultdict(lambda: []) - self.created_regions: Dict[str, Region] = dict() + self.two_way_entrance_register: Dict[Tuple[str, str], List[Entrance]] = defaultdict(lambda: []) + self.created_region_names: Set[str] = set() diff --git a/worlds/witness/rules.py b/worlds/witness/rules.py index 6445545e9b7a..b4982d1830b2 100644 --- a/worlds/witness/rules.py +++ b/worlds/witness/rules.py @@ -2,15 +2,14 @@ Defines the rules by which locations can be accessed, depending on the items received """ - -from typing import TYPE_CHECKING, FrozenSet +from typing import TYPE_CHECKING from BaseClasses import CollectionState from worlds.generic.Rules import CollectionRule, set_rule -from . import WitnessPlayerRegions from .data import static_logic as static_witness_logic +from .data.utils import WitnessRule from .locations import WitnessPlayerLocations from .player_logic import WitnessPlayerLogic @@ -32,8 +31,7 @@ ] -def _has_laser(laser_hex: str, world: "WitnessWorld", player: int, - redirect_required: bool) -> CollectionRule: +def _has_laser(laser_hex: str, world: "WitnessWorld", player: int, redirect_required: bool) -> CollectionRule: if laser_hex == "0x012FB" and redirect_required: return lambda state: ( _can_solve_panel(laser_hex, world, world.player, world.player_logic, world.player_locations)(state) @@ -69,95 +67,164 @@ def _can_solve_panel(panel: str, world: "WitnessWorld", player: int, player_logi return make_lambda(panel, world) -def _can_move_either_direction(state: CollectionState, source: str, target: str, - player_regions: WitnessPlayerRegions) -> bool: - entrance_forward = player_regions.created_entrances[source, target] - entrance_backward = player_regions.created_entrances[target, source] +def _can_do_expert_pp2(state: CollectionState, world: "WitnessWorld") -> bool: + """ + For Expert PP2, you need a way to access PP2 from the front, and a separate way from the back. + This condition is quite complicated. We'll attempt to evaluate it as lazily as possible. + """ + + player = world.player + player_regions = world.player_regions - return ( - any(entrance.can_reach(state) for entrance in entrance_forward) - or - any(entrance.can_reach(state) for entrance in entrance_backward) + front_access = ( + any(e.can_reach(state) for e in player_regions.two_way_entrance_register["Keep 2nd Pressure Plate", "Keep"]) + and state.can_reach_region("Keep", player) ) + # If we don't have front access, we can't do PP2. + if not front_access: + return False -def _can_do_expert_pp2(state: CollectionState, world: "WitnessWorld") -> bool: - player = world.player + # Front access works. Now, we need to check for the many ways to access PP2 from the back. + # All of those ways lead through the PP3 exit door from PP4. So we check this first. + + fourth_to_third = any(e.can_reach(state) for e in player_regions.two_way_entrance_register[ + "Keep 3rd Pressure Plate", "Keep 4th Pressure Plate" + ]) + + # If we can't get from PP4 to PP3, we can't do PP2. + if not fourth_to_third: + return False - hedge_2_access = ( - _can_move_either_direction(state, "Keep 2nd Maze", "Keep", world.player_regions) + # We can go from PP4 to PP3. We now need to find a way to PP4. + # The shadows shortcut is the simplest way. + + shadows_shortcut = ( + any(e.can_reach(state) for e in player_regions.two_way_entrance_register["Keep 4th Pressure Plate", "Shadows"]) ) - hedge_3_access = ( - _can_move_either_direction(state, "Keep 3rd Maze", "Keep", world.player_regions) - or _can_move_either_direction(state, "Keep 3rd Maze", "Keep 2nd Maze", world.player_regions) - and hedge_2_access + if shadows_shortcut: + return True + + # We don't have the Shadows shortcut. This means we need to come in through the PP4 exit door instead. + + tower_to_pp4 = any( + e.can_reach(state) for e in player_regions.two_way_entrance_register["Keep 4th Pressure Plate", "Keep Tower"] ) - hedge_4_access = ( - _can_move_either_direction(state, "Keep 4th Maze", "Keep", world.player_regions) - or _can_move_either_direction(state, "Keep 4th Maze", "Keep 3rd Maze", world.player_regions) - and hedge_3_access + # If we don't have the PP4 exit door, we've run out of options. + if not tower_to_pp4: + return False + + # We have the PP4 exit door. If we can get to Keep Tower from behind, we can do PP2. + # The simplest way would be the Tower Shortcut. + + tower_shortcut = any(e.can_reach(state) for e in player_regions.two_way_entrance_register["Keep", "Keep Tower"]) + + if tower_shortcut: + return True + + # We don't have the Tower shortcut. At this point, there is one possibility remaining: + # Getting to Keep Tower through the hedge mazes. This can be done in a multitude of ways. + # No matter what, though, we would need Hedge Maze 4 Exit to Keep Tower. + + tower_access_from_hedges = any( + e.can_reach(state) for e in player_regions.two_way_entrance_register["Keep 4th Maze", "Keep Tower"] ) - hedge_access = ( - _can_move_either_direction(state, "Keep 4th Maze", "Keep Tower", world.player_regions) - and state.can_reach("Keep", "Region", player) - and hedge_4_access + if not tower_access_from_hedges: + return False + + # We can reach Keep Tower from Hedge Maze 4. If we now have the Hedge 4 Shortcut, we are immediately good. + + hedge_4_shortcut = any( + e.can_reach(state) for e in player_regions.two_way_entrance_register["Keep 4th Maze", "Keep"] ) - backwards_to_fourth = ( - state.can_reach("Keep", "Region", player) - and _can_move_either_direction(state, "Keep 4th Pressure Plate", "Keep Tower", world.player_regions) - and ( - _can_move_either_direction(state, "Keep", "Keep Tower", world.player_regions) - or hedge_access - ) + # If we have the hedge 4 shortcut, that works. + if hedge_4_shortcut: + return True + + # We don't have the hedge 4 shortcut. This means we would now need to come through Hedge Maze 3. + + hedge_3_to_4 = any( + e.can_reach(state) for e in player_regions.two_way_entrance_register["Keep 4th Maze", "Keep 3rd Maze"] ) - shadows_shortcut = ( - state.can_reach("Main Island", "Region", player) - and _can_move_either_direction(state, "Keep 4th Pressure Plate", "Shadows", world.player_regions) + if not hedge_3_to_4: + return False + + # We can get to Hedge 4 from Hedge 3. If we have the Hedge 3 Shortcut, we're good. + + hedge_3_shortcut = any( + e.can_reach(state) for e in player_regions.two_way_entrance_register["Keep 3rd Maze", "Keep"] ) - backwards_access = ( - _can_move_either_direction(state, "Keep 3rd Pressure Plate", "Keep 4th Pressure Plate", world.player_regions) - and (backwards_to_fourth or shadows_shortcut) + if hedge_3_shortcut: + return True + + # We don't have Hedge 3 Shortcut. This means we would now need to come through Hedge Maze 2. + + hedge_2_to_3 = any( + e.can_reach(state) for e in player_regions.two_way_entrance_register["Keep 3rd Maze", "Keep 2nd Maze"] ) - front_access = ( - _can_move_either_direction(state, "Keep 2nd Pressure Plate", "Keep", world.player_regions) - and state.can_reach("Keep", "Region", player) + if not hedge_2_to_3: + return False + + # We can get to Hedge 3 from Hedge 2. If we can get from Keep to Hedge 2, we're good. + # This covers both Hedge 1 Exit and Hedge 2 Shortcut, because Hedge 1 is just part of the Keep region. + + hedge_2_from_keep = any( + e.can_reach(state) for e in player_regions.two_way_entrance_register["Keep 2nd Maze", "Keep"] ) - return front_access and backwards_access + return hedge_2_from_keep def _can_do_theater_to_tunnels(state: CollectionState, world: "WitnessWorld") -> bool: + """ + To do Tunnels Theater Flowers EP, you need to quickly move from Theater to Tunnels. + This condition is a little tricky. We'll attempt to evaluate it as lazily as possible. + """ + + # Checking for access to Theater is not necessary, as solvability of Tutorial Video is checked in the other half + # of the Theater Flowers EP condition. + + player_regions = world.player_regions + direct_access = ( - _can_move_either_direction(state, "Tunnels", "Windmill Interior", world.player_regions) - and _can_move_either_direction(state, "Theater", "Windmill Interior", world.player_regions) + any(e.can_reach(state) for e in player_regions.two_way_entrance_register["Tunnels", "Windmill Interior"]) + and any(e.can_reach(state) for e in player_regions.two_way_entrance_register["Theater", "Windmill Interior"]) ) - theater_from_town = ( - _can_move_either_direction(state, "Town", "Windmill Interior", world.player_regions) - and _can_move_either_direction(state, "Theater", "Windmill Interior", world.player_regions) - or _can_move_either_direction(state, "Town", "Theater", world.player_regions) - ) + if direct_access: + return True + + # We don't have direct access through the shortest path. + # This means we somehow need to exit Theater to the Main Island, and then enter Tunnels from the Main Island. + # Getting to Tunnels through Mountain -> Caves -> Tunnels is way too slow, so we only expect paths through Town. + + # We need a way from Theater to Town. This is actually guaranteed, otherwise we wouldn't be in Theater. + # The only ways to Theater are through Town and Tunnels. We just checked the Tunnels way. + # This might need to be changed when warps are implemented. + + # We also need a way from Town to Tunnels. tunnels_from_town = ( - _can_move_either_direction(state, "Tunnels", "Windmill Interior", world.player_regions) - and _can_move_either_direction(state, "Town", "Windmill Interior", world.player_regions) - or _can_move_either_direction(state, "Tunnels", "Town", world.player_regions) + any(e.can_reach(state) for e in player_regions.two_way_entrance_register["Tunnels", "Windmill Interior"]) + and any(e.can_reach(state) for e in player_regions.two_way_entrance_register["Town", "Windmill Interior"]) + or any(e.can_reach(state) for e in player_regions.two_way_entrance_register["Tunnels", "Town"]) ) - return direct_access or theater_from_town and tunnels_from_town + return tunnels_from_town def _has_item(item: str, world: "WitnessWorld", player: int, player_logic: WitnessPlayerLogic, player_locations: WitnessPlayerLocations) -> CollectionRule: if item in player_logic.REFERENCE_LOGIC.ALL_REGIONS_BY_NAME: - return lambda state: state.can_reach(item, "Region", player) + region = world.get_region(item) + return region.can_reach if item == "7 Lasers": laser_req = world.options.mountain_lasers.value return _has_lasers(laser_req, world, False) @@ -181,8 +248,7 @@ def _has_item(item: str, world: "WitnessWorld", player: int, return lambda state: state.has(prog_item, player, player_logic.MULTI_AMOUNTS[item]) -def _meets_item_requirements(requirements: FrozenSet[FrozenSet[str]], - world: "WitnessWorld") -> CollectionRule: +def _meets_item_requirements(requirements: WitnessRule, world: "WitnessWorld") -> CollectionRule: """ Checks whether item and panel requirements are met for a panel From dedabad290ccec512776f732c3619509d1d091b1 Mon Sep 17 00:00:00 2001 From: Emily <35015090+EmilyV99@users.noreply.github.com> Date: Sun, 2 Jun 2024 12:45:46 -0400 Subject: [PATCH 56/64] APSudoku: take over maintaining hintgame sudoku from bk_sudoku (#3432) --- docs/CODEOWNERS | 6 ++-- worlds/{bk_sudoku => apsudoku}/__init__.py | 25 +++++---------- worlds/apsudoku/docs/en_Sudoku.md | 13 ++++++++ worlds/apsudoku/docs/setup_en.md | 37 ++++++++++++++++++++++ worlds/bk_sudoku/docs/de_Sudoku.md | 21 ------------ worlds/bk_sudoku/docs/en_Sudoku.md | 13 -------- worlds/bk_sudoku/docs/setup_de.md | 27 ---------------- worlds/bk_sudoku/docs/setup_en.md | 24 -------------- 8 files changed, 61 insertions(+), 105 deletions(-) rename worlds/{bk_sudoku => apsudoku}/__init__.py (50%) create mode 100644 worlds/apsudoku/docs/en_Sudoku.md create mode 100644 worlds/apsudoku/docs/setup_en.md delete mode 100644 worlds/bk_sudoku/docs/de_Sudoku.md delete mode 100644 worlds/bk_sudoku/docs/en_Sudoku.md delete mode 100644 worlds/bk_sudoku/docs/setup_de.md delete mode 100644 worlds/bk_sudoku/docs/setup_en.md diff --git a/docs/CODEOWNERS b/docs/CODEOWNERS index f54132e24aa0..10b962d49970 100644 --- a/docs/CODEOWNERS +++ b/docs/CODEOWNERS @@ -15,15 +15,15 @@ # A Link to the Past /worlds/alttp/ @Berserker66 +# Sudoku (APSudoku) +/worlds/apsudoku/ @EmilyV99 + # Aquaria /worlds/aquaria/ @tioui # ArchipIDLE /worlds/archipidle/ @LegendaryLinux -# Sudoku (BK Sudoku) -/worlds/bk_sudoku/ @Jarno458 - # Blasphemous /worlds/blasphemous/ @TRPG0 diff --git a/worlds/bk_sudoku/__init__.py b/worlds/apsudoku/__init__.py similarity index 50% rename from worlds/bk_sudoku/__init__.py rename to worlds/apsudoku/__init__.py index 2c57bc7301ff..c6bd02bdc262 100644 --- a/worlds/bk_sudoku/__init__.py +++ b/worlds/apsudoku/__init__.py @@ -3,41 +3,32 @@ from BaseClasses import Tutorial from ..AutoWorld import WebWorld, World - -class Bk_SudokuWebWorld(WebWorld): +class AP_SudokuWebWorld(WebWorld): options_page = "games/Sudoku/info/en" theme = 'partyTime' setup_en = Tutorial( tutorial_name='Setup Guide', - description='A guide to playing BK Sudoku', + description='A guide to playing APSudoku', language='English', file_name='setup_en.md', link='setup/en', - authors=['Jarno'] - ) - setup_de = Tutorial( - tutorial_name='Setup Anleitung', - description='Eine Anleitung um BK-Sudoku zu spielen', - language='Deutsch', - file_name='setup_de.md', - link='setup/de', - authors=['Held_der_Zeit'] + authors=['EmilyV'] ) - tutorials = [setup_en, setup_de] + tutorials = [setup_en] - -class Bk_SudokuWorld(World): +class AP_SudokuWorld(World): """ Play a little Sudoku while you're in BK mode to maybe get some useful hints """ game = "Sudoku" - web = Bk_SudokuWebWorld() + web = AP_SudokuWebWorld() item_name_to_id: Dict[str, int] = {} location_name_to_id: Dict[str, int] = {} @classmethod def stage_assert_generate(cls, multiworld): - raise Exception("BK Sudoku cannot be used for generating worlds, the client can instead connect to any other world") + raise Exception("APSudoku cannot be used for generating worlds, the client can instead connect to any slot from any world") + diff --git a/worlds/apsudoku/docs/en_Sudoku.md b/worlds/apsudoku/docs/en_Sudoku.md new file mode 100644 index 000000000000..e81f773e0291 --- /dev/null +++ b/worlds/apsudoku/docs/en_Sudoku.md @@ -0,0 +1,13 @@ +# APSudoku + +## Hint Games + +HintGames do not need to be added at the start of a seed, and do not create a 'slot'- instead, you connect the HintGame client to a different game's slot. By playing a HintGame, you can earn hints for the connected slot. + +## What is this game? + +Play Sudoku puzzles of varying difficulties, earning a hint for each puzzle correctly solved. Harder puzzles are more likely to grant a hint towards a Progression item, though otherwise what hint is granted is random. + +## Where is the options page? + +There is no options page; this game cannot be used in your .yamls. Instead, the client can connect to any slot in a multiworld. diff --git a/worlds/apsudoku/docs/setup_en.md b/worlds/apsudoku/docs/setup_en.md new file mode 100644 index 000000000000..cf2c755bd837 --- /dev/null +++ b/worlds/apsudoku/docs/setup_en.md @@ -0,0 +1,37 @@ +# APSudoku Setup Guide + +## Required Software +- [APSudoku](https://github.com/EmilyV99/APSudoku) +- Windows (most tested on Win10) +- Other platforms might be able to build from source themselves; and may be included in the future. + +## General Concept + +This is a HintGame client, which can connect to any multiworld slot, allowing you to play Sudoku to unlock random hints for that slot's locations. + +Does not need to be added at the start of a seed, as it does not create any slots of its own, nor does it have any YAML files. + +## Installation Procedures + +Go to the latest release from the [APSudoku Releases page](https://github.com/EmilyV99/APSudoku/releases). Download and extract the `APSudoku.zip` file. + +## Joining a MultiWorld Game + +1. Run APSudoku.exe +2. Under the 'Archipelago' tab at the top-right: + - Enter the server url & port number + - Enter the name of the slot you wish to connect to + - Enter the room password (optional) + - Select DeathLink related settings (optional) + - Press connect +3. Go back to the 'Sudoku' tab + - Click the various '?' buttons for information on how to play / control +4. Choose puzzle difficulty +5. Try to solve the Sudoku. Click 'Check' when done. + +## DeathLink Support + +If 'DeathLink' is enabled when you click 'Connect': +- Lose a life if you check an incorrect puzzle (not an _incomplete_ puzzle- if any cells are empty, you get off with a warning), or quit a puzzle without solving it (including disconnecting). +- Life count customizable (default 0). Dying with 0 lives left kills linked players AND resets your puzzle. +- On receiving a DeathLink from another player, your puzzle resets. diff --git a/worlds/bk_sudoku/docs/de_Sudoku.md b/worlds/bk_sudoku/docs/de_Sudoku.md deleted file mode 100644 index abb50c5498d1..000000000000 --- a/worlds/bk_sudoku/docs/de_Sudoku.md +++ /dev/null @@ -1,21 +0,0 @@ -# BK-Sudoku - -## Was ist das für ein Spiel? - -BK-Sudoku ist kein typisches Archipelago-Spiel; stattdessen ist es ein gewöhnlicher Sudoku-Client der sich zu jeder -beliebigen Multiworld verbinden kann. Einmal verbunden kannst du ein 9x9 Sudoku spielen um einen zufälligen Hinweis -für dein Spiel zu erhalten. Es ist zwar langsam, aber es gibt dir etwas zu tun, solltest du mal nicht in der Lage sein -weitere „Checks” zu erreichen. -(Wer mag kann auch einfach so Sudoku spielen. Man muss nicht mit einer Multiworld verbunden sein, um ein Sudoku zu -spielen/generieren.) - -## Wie werden Hinweise freigeschalten? - -Nach dem Lösen eines Sudokus wird für den verbundenen Slot ein zufällig ausgewählter Hinweis freigegeben, für einen -Gegenstand der noch nicht gefunden wurde. - -## Wo ist die Seite für die Einstellungen? - -Es gibt keine Seite für die Einstellungen. Dieses Spiel kann nicht in deinen YAML-Dateien benutzt werden. Stattdessen -kann sich der Client mit einem beliebigen Slot einer Multiworld verbinden. In dem Client selbst kann aber der -Schwierigkeitsgrad des Sudoku ausgewählt werden. diff --git a/worlds/bk_sudoku/docs/en_Sudoku.md b/worlds/bk_sudoku/docs/en_Sudoku.md deleted file mode 100644 index dae5a9e3e513..000000000000 --- a/worlds/bk_sudoku/docs/en_Sudoku.md +++ /dev/null @@ -1,13 +0,0 @@ -# Bk Sudoku - -## What is this game? - -BK Sudoku is not a typical Archipelago game; instead, it is a generic Sudoku client that can connect to any existing multiworld. When connected, you can play Sudoku to unlock random hints for your game. While slow, it will give you something to do when you can't reach the checks in your game. - -## What hints are unlocked? - -After completing a Sudoku puzzle, the game will unlock 1 random hint for an unchecked location in the slot you are connected to. - -## Where is the options page? - -There is no options page; this game cannot be used in your .yamls. Instead, the client can connect to any slot in a multiworld. diff --git a/worlds/bk_sudoku/docs/setup_de.md b/worlds/bk_sudoku/docs/setup_de.md deleted file mode 100644 index 71a8e5f6245d..000000000000 --- a/worlds/bk_sudoku/docs/setup_de.md +++ /dev/null @@ -1,27 +0,0 @@ -# BK-Sudoku Setup Anleitung - -## Benötigte Software -- [Bk-Sudoku](https://github.com/Jarno458/sudoku) -- Windows 8 oder höher - -## Generelles Konzept - -Dies ist ein Client, der sich mit jedem beliebigen Slot einer Multiworld verbinden kann. Er lässt dich ein (9x9) Sudoku -spielen, um zufällige Hinweise für den verbundenen Slot freizuschalten. - -Aufgrund des Fakts, dass der Sudoku-Client sich zu jedem beliebigen Slot verbinden kann, ist es daher nicht notwendig -eine YAML für dieses Spiel zu generieren, da es keinen neuen Slot zur Multiworld-Session hinzufügt. - -## Installationsprozess - -Gehe zu der aktuellsten (latest) Veröffentlichung der [BK-Sudoku Releases](https://github.com/Jarno458/sudoku/releases). -Downloade und extrahiere/entpacke die `Bk_Sudoku.zip`-Datei. - -## Verbinden mit einer Multiworld - -1. Starte `Bk_Sudoku.exe` -2. Trage den Namen des Slots ein, mit dem du dich verbinden möchtest -3. Trage die Server-URL und den Port ein -4. Drücke auf Verbinden (connect) -5. Wähle deinen Schwierigkeitsgrad -6. Versuche das Sudoku zu Lösen diff --git a/worlds/bk_sudoku/docs/setup_en.md b/worlds/bk_sudoku/docs/setup_en.md deleted file mode 100644 index eda17e701bb8..000000000000 --- a/worlds/bk_sudoku/docs/setup_en.md +++ /dev/null @@ -1,24 +0,0 @@ -# BK Sudoku Setup Guide - -## Required Software -- [Bk Sudoku](https://github.com/Jarno458/sudoku) -- Windows 8 or higher - -## General Concept - -This is a client that can connect to any multiworld slot, and lets you play Sudoku to unlock random hints for that slot's locations. - -Due to the fact that the Sudoku client may connect to any slot, it is not necessary to generate a YAML for this game as it does not generate any new slots in the multiworld session. - -## Installation Procedures - -Go to the latest release on [BK Sudoku Releases](https://github.com/Jarno458/sudoku/releases). Download and extract the `Bk_Sudoku.zip` file. - -## Joining a MultiWorld Game - -1. Run Bk_Sudoku.exe -2. Enter the name of the slot you wish to connect to -3. Enter the server url & port number -4. Press connect -5. Choose difficulty -6. Try to solve the Sudoku From 6432560fe5643aec302c4cc96761a12a03a5b8a2 Mon Sep 17 00:00:00 2001 From: qwint Date: Sun, 2 Jun 2024 21:39:34 -0500 Subject: [PATCH 57/64] Fix Egg_Shop typo in costsanity (#3447) --- worlds/hk/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worlds/hk/__init__.py b/worlds/hk/__init__.py index fdaece8d34cd..78287305df5f 100644 --- a/worlds/hk/__init__.py +++ b/worlds/hk/__init__.py @@ -405,7 +405,7 @@ def _compute_weights(weights: dict, desc: str) -> typing.Dict[str, int]: continue if setting == CostSanity.option_shopsonly and location.basename not in multi_locations: continue - if location.basename in {'Grubfather', 'Seer', 'Eggshop'}: + if location.basename in {'Grubfather', 'Seer', 'Egg_Shop'}: our_weights = dict(weights_geoless) else: our_weights = dict(weights) From 424c8b0be9654f9c8556c1e68fcc093d00f860c6 Mon Sep 17 00:00:00 2001 From: Remy Jette Date: Sun, 2 Jun 2024 22:42:15 -0400 Subject: [PATCH 58/64] Pokemon RB: Add an item group for each HM to improve hinting (#3311) * Pokemon RB: Add an item group for each HM HMs are suffixed with the name of the move, e.g. "HM02 Fly". If TM move are randomized, they do not have the move name, e.g. "TM02". If someone hints for an HM using the just the number, the fuzzy matching sees "TM02" as closer than "HM02 Fly", and in fact sees it as close enough to not ask the user to confirm, leading them to waste hint points on non-progression item that they didn't intend. Emerald already does this for this reason, adding the same for RB. * Add the new groups for HMs in the item_table instead --- worlds/pokemon_rb/items.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/worlds/pokemon_rb/items.py b/worlds/pokemon_rb/items.py index 24cad13252b1..de29f341c6df 100644 --- a/worlds/pokemon_rb/items.py +++ b/worlds/pokemon_rb/items.py @@ -119,11 +119,11 @@ def __init__(self, item_id, classification, groups): "Card Key 11F": ItemData(109, ItemClassification.progression, ["Unique", "Key Items", "Card Keys"]), "Progressive Card Key": ItemData(110, ItemClassification.progression, ["Unique", "Key Items", "Card Keys"]), "Sleep Trap": ItemData(111, ItemClassification.trap, ["Traps"]), - "HM01 Cut": ItemData(196, ItemClassification.progression, ["Unique", "HMs", "Key Items"]), - "HM02 Fly": ItemData(197, ItemClassification.progression, ["Unique", "HMs", "Key Items"]), - "HM03 Surf": ItemData(198, ItemClassification.progression, ["Unique", "HMs", "Key Items"]), - "HM04 Strength": ItemData(199, ItemClassification.progression, ["Unique", "HMs", "Key Items"]), - "HM05 Flash": ItemData(200, ItemClassification.progression, ["Unique", "HMs", "Key Items"]), + "HM01 Cut": ItemData(196, ItemClassification.progression, ["Unique", "HMs", "HM01", "Key Items"]), + "HM02 Fly": ItemData(197, ItemClassification.progression, ["Unique", "HMs", "HM02", "Key Items"]), + "HM03 Surf": ItemData(198, ItemClassification.progression, ["Unique", "HMs", "HM03", "Key Items"]), + "HM04 Strength": ItemData(199, ItemClassification.progression, ["Unique", "HMs", "HM04", "Key Items"]), + "HM05 Flash": ItemData(200, ItemClassification.progression, ["Unique", "HMs", "HM05", "Key Items"]), "TM01 Mega Punch": ItemData(201, ItemClassification.useful, ["Unique", "TMs"]), "TM02 Razor Wind": ItemData(202, ItemClassification.filler, ["Unique", "TMs"]), "TM03 Swords Dance": ItemData(203, ItemClassification.useful, ["Unique", "TMs"]), From d9120f0bea7fb564c51d3257e2ed67624d73261f Mon Sep 17 00:00:00 2001 From: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> Date: Mon, 3 Jun 2024 04:42:27 -0400 Subject: [PATCH 59/64] WebHost: Allowing options that work on WebHost to be used in presets (#3441) --- WebHostLib/options.py | 2 +- test/webhost/test_option_presets.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/WebHostLib/options.py b/WebHostLib/options.py index 62ba86a56626..53c3a6151b82 100644 --- a/WebHostLib/options.py +++ b/WebHostLib/options.py @@ -91,7 +91,7 @@ def option_presets(game: str) -> Response: f"Expected {option.special_range_names.keys()} or {option.range_start}-{option.range_end}." presets[preset_name][preset_option_name] = option.value - elif isinstance(option, Options.Range): + elif isinstance(option, (Options.Range, Options.OptionSet, Options.OptionList, Options.ItemDict)): presets[preset_name][preset_option_name] = option.value elif isinstance(preset_option, str): # Ensure the option value is valid for Choice and Toggle options diff --git a/test/webhost/test_option_presets.py b/test/webhost/test_option_presets.py index 0c88b6c2ee6f..b0af8a871183 100644 --- a/test/webhost/test_option_presets.py +++ b/test/webhost/test_option_presets.py @@ -1,7 +1,7 @@ import unittest from worlds import AutoWorldRegister -from Options import Choice, NamedRange, Toggle, Range +from Options import ItemDict, NamedRange, NumericOption, OptionList, OptionSet class TestOptionPresets(unittest.TestCase): @@ -14,7 +14,7 @@ def test_option_presets_have_valid_options(self): with self.subTest(game=game_name, preset=preset_name, option=option_name): try: option = world_type.options_dataclass.type_hints[option_name].from_any(option_value) - supported_types = [Choice, Toggle, Range, NamedRange] + supported_types = [NumericOption, OptionSet, OptionList, ItemDict] if not any([issubclass(option.__class__, t) for t in supported_types]): self.fail(f"'{option_name}' in preset '{preset_name}' for game '{game_name}' " f"is not a supported type for webhost. " From 70e9ccb13c600072b234027a944a9a190835c37a Mon Sep 17 00:00:00 2001 From: Scipio Wright Date: Mon, 3 Jun 2024 04:44:37 -0400 Subject: [PATCH 60/64] TUNIC: Fix plando connections, seed groups, and UT support (#3429) --- worlds/tunic/__init__.py | 10 ++++++---- worlds/tunic/er_scripts.py | 33 +++++++++++++++++++-------------- worlds/tunic/options.py | 4 ++-- 3 files changed, 27 insertions(+), 20 deletions(-) diff --git a/worlds/tunic/__init__.py b/worlds/tunic/__init__.py index 9ef5800955aa..624208da3a0b 100644 --- a/worlds/tunic/__init__.py +++ b/worlds/tunic/__init__.py @@ -8,7 +8,7 @@ from .regions import tunic_regions from .er_scripts import create_er_regions from .er_data import portal_mapping -from .options import TunicOptions, EntranceRando, tunic_option_groups, tunic_option_presets +from .options import TunicOptions, EntranceRando, tunic_option_groups, tunic_option_presets, TunicPlandoConnections from worlds.AutoWorld import WebWorld, World from Options import PlandoConnection from decimal import Decimal, ROUND_HALF_UP @@ -43,7 +43,7 @@ class SeedGroup(TypedDict): logic_rules: int # logic rules value laurels_at_10_fairies: bool # laurels location value fixed_shop: bool # fixed shop value - plando: List[PlandoConnection] # consolidated list of plando connections for the seed group + plando: TunicPlandoConnections # consolidated of plando connections for the seed group class TunicWorld(World): @@ -96,13 +96,15 @@ def generate_early(self) -> None: self.options.hexagon_quest.value = passthrough["hexagon_quest"] self.options.entrance_rando.value = passthrough["entrance_rando"] self.options.shuffle_ladders.value = passthrough["shuffle_ladders"] + self.options.fixed_shop.value = self.options.fixed_shop.option_false + self.options.laurels_location.value = self.options.laurels_location.option_anywhere @classmethod def stage_generate_early(cls, multiworld: MultiWorld) -> None: tunic_worlds: Tuple[TunicWorld] = multiworld.get_game_worlds("TUNIC") for tunic in tunic_worlds: # if it's one of the options, then it isn't a custom seed group - if tunic.options.entrance_rando.value in EntranceRando.options: + if tunic.options.entrance_rando.value in EntranceRando.options.values(): continue group = tunic.options.entrance_rando.value # if this is the first world in the group, set the rules equal to its rules @@ -147,7 +149,7 @@ def stage_generate_early(cls, multiworld: MultiWorld) -> None: f"{tunic.multiworld.get_player_name(tunic.player)}'s plando " f"connection {cxn.entrance} <-> {cxn.exit}") if new_cxn: - cls.seed_groups[group]["plando"].append(cxn) + cls.seed_groups[group]["plando"].value.append(cxn) def create_item(self, name: str) -> TunicItem: item_data = item_table[name] diff --git a/worlds/tunic/er_scripts.py b/worlds/tunic/er_scripts.py index 7e022c9f3a0d..9d25137ba469 100644 --- a/worlds/tunic/er_scripts.py +++ b/worlds/tunic/er_scripts.py @@ -140,7 +140,7 @@ def pair_portals(world: "TunicWorld") -> Dict[Portal, Portal]: waterfall_plando = False # if it's not one of the EntranceRando options, it's a custom seed - if world.options.entrance_rando.value not in EntranceRando.options: + if world.options.entrance_rando.value not in EntranceRando.options.values(): seed_group = world.seed_groups[world.options.entrance_rando.value] logic_rules = seed_group["logic_rules"] fixed_shop = seed_group["fixed_shop"] @@ -162,6 +162,11 @@ def pair_portals(world: "TunicWorld") -> Dict[Portal, Portal]: portal_map.remove(portal) break + # If using Universal Tracker, restore portal_map. Could be cleaner, but it does not matter for UT even a little bit + if hasattr(world.multiworld, "re_gen_passthrough"): + if "TUNIC" in world.multiworld.re_gen_passthrough: + portal_map = portal_mapping.copy() + # create separate lists for dead ends and non-dead ends for portal in portal_map: dead_end_status = tunic_er_regions[portal.region].dead_end @@ -193,7 +198,7 @@ def pair_portals(world: "TunicWorld") -> Dict[Portal, Portal]: connected_regions.add(start_region) connected_regions = update_reachable_regions(connected_regions, traversal_reqs, has_laurels, logic_rules) - if world.options.entrance_rando.value in EntranceRando.options: + if world.options.entrance_rando.value in EntranceRando.options.values(): plando_connections = world.options.plando_connections.value else: plando_connections = world.seed_groups[world.options.entrance_rando.value]["plando"] @@ -255,7 +260,7 @@ def pair_portals(world: "TunicWorld") -> Dict[Portal, Portal]: else: # if not both, they're both dead ends if not portal2: - if world.options.entrance_rando.value not in EntranceRando.options: + if world.options.entrance_rando.value not in EntranceRando.options.values(): raise Exception(f"Tunic ER seed group {world.options.entrance_rando.value} paired a dead " "end to a dead end in their plando connections.") else: @@ -302,21 +307,21 @@ def pair_portals(world: "TunicWorld") -> Dict[Portal, Portal]: traversal_reqs.setdefault(portal1.region, dict())[portal2.region] = [] traversal_reqs.setdefault(portal2.region, dict())[portal1.region] = [] - if portal1.region == "Zig Skip Exit" or portal2.region == "Zig Skip Exit": - if portal1_dead_end or portal2_dead_end or \ - portal1.region == "Secret Gathering Place" or portal2.region == "Secret Gathering Place": - if world.options.entrance_rando.value not in EntranceRando.options: - raise Exception(f"Tunic ER seed group {world.options.entrance_rando.value} paired a dead " - "end to a dead end in their plando connections.") - else: - raise Exception(f"{player_name} paired a dead end to a dead end in their " - "plando connections.") + if (portal1.region == "Zig Skip Exit" and (portal2_dead_end or portal2.region == "Secret Gathering Place") + or portal2.region == "Zig Skip Exit" and (portal1_dead_end or portal1.region == "Secret Gathering Place")): + if world.options.entrance_rando.value not in EntranceRando.options.values(): + raise Exception(f"Tunic ER seed group {world.options.entrance_rando.value} paired a dead " + "end to a dead end in their plando connections.") + else: + raise Exception(f"{player_name} paired a dead end to a dead end in their " + "plando connections.") - if portal1.region == "Secret Gathering Place" or portal2.region == "Secret Gathering Place": + if (portal1.region == "Secret Gathering Place" and (portal2_dead_end or portal2.region == "Zig Skip Exit") + or portal2.region == "Secret Gathering Place" and (portal1_dead_end or portal1.region == "Zig Skip Exit")): # need to make sure you didn't pair this to a dead end or zig skip if portal1_dead_end or portal2_dead_end or \ portal1.region == "Zig Skip Exit" or portal2.region == "Zig Skip Exit": - if world.options.entrance_rando.value not in EntranceRando.options: + if world.options.entrance_rando.value not in EntranceRando.options.values(): raise Exception(f"Tunic ER seed group {world.options.entrance_rando.value} paired a dead " "end to a dead end in their plando connections.") else: diff --git a/worlds/tunic/options.py b/worlds/tunic/options.py index b3b6b3b96fb0..ff9872ab4807 100644 --- a/worlds/tunic/options.py +++ b/worlds/tunic/options.py @@ -173,7 +173,7 @@ class ShuffleLadders(Toggle): display_name = "Shuffle Ladders" -class TUNICPlandoConnections(PlandoConnections): +class TunicPlandoConnections(PlandoConnections): entrances = {*(portal.name for portal in portal_mapping), "Shop", "Shop Portal"} exits = {*(portal.name for portal in portal_mapping), "Shop", "Shop Portal"} @@ -198,7 +198,7 @@ class TunicOptions(PerGameCommonOptions): lanternless: Lanternless maskless: Maskless laurels_location: LaurelsLocation - plando_connections: TUNICPlandoConnections + plando_connections: TunicPlandoConnections tunic_option_groups = [ From cff7327558979c3088ff5ac792a4e3d0149fd4e9 Mon Sep 17 00:00:00 2001 From: Zach Parks Date: Mon, 3 Jun 2024 03:45:01 -0500 Subject: [PATCH 61/64] Utils: Fix mistake made with `KeyedDefaultDict` from #1933 that broke tracker functionality. (#3433) --- Utils.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/Utils.py b/Utils.py index eea81a2d3201..a7fd7f4f334c 100644 --- a/Utils.py +++ b/Utils.py @@ -458,8 +458,14 @@ class KeyedDefaultDict(collections.defaultdict): """defaultdict variant that uses the missing key as argument to default_factory""" default_factory: typing.Callable[[typing.Any], typing.Any] - def __init__(self, default_factory: typing.Callable[[Any], Any] = None, **kwargs): - super().__init__(default_factory, **kwargs) + def __init__(self, + default_factory: typing.Callable[[Any], Any] = None, + seq: typing.Union[typing.Mapping, typing.Iterable, None] = None, + **kwargs): + if seq is not None: + super().__init__(default_factory, seq, **kwargs) + else: + super().__init__(default_factory, **kwargs) def __missing__(self, key): self[key] = value = self.default_factory(key) From fb2c194e3733c3ba200cfff52bf89ddb4c4a6912 Mon Sep 17 00:00:00 2001 From: Star Rauchenberger Date: Mon, 3 Jun 2024 04:51:27 -0400 Subject: [PATCH 62/64] Lingo: Fix Basement access with THE MASTER (#3231) --- worlds/lingo/player_logic.py | 19 ++++++++++++++----- worlds/lingo/regions.py | 9 ++++++++- worlds/lingo/rules.py | 9 +++------ worlds/lingo/test/TestMastery.py | 19 ++++++++++++++++++- 4 files changed, 43 insertions(+), 13 deletions(-) diff --git a/worlds/lingo/player_logic.py b/worlds/lingo/player_logic.py index b6941f37eed1..1621620e1e14 100644 --- a/worlds/lingo/player_logic.py +++ b/worlds/lingo/player_logic.py @@ -18,19 +18,23 @@ class AccessRequirements: rooms: Set[str] doors: Set[RoomAndDoor] colors: Set[str] + the_master: bool def __init__(self): self.rooms = set() self.doors = set() self.colors = set() + self.the_master = False def merge(self, other: "AccessRequirements"): self.rooms |= other.rooms self.doors |= other.doors self.colors |= other.colors + self.the_master |= other.the_master def __str__(self): - return f"AccessRequirements(rooms={self.rooms}, doors={self.doors}, colors={self.colors})" + return f"AccessRequirements(rooms={self.rooms}, doors={self.doors}, colors={self.colors})," \ + f" the_master={self.the_master}" class PlayerLocation(NamedTuple): @@ -463,6 +467,9 @@ def calculate_panel_requirements(self, room: str, panel: str, world: "LingoWorld req_panel.panel, world) access_reqs.merge(sub_access_reqs) + if panel == "THE MASTER": + access_reqs.the_master = True + self.panel_reqs[room][panel] = access_reqs return self.panel_reqs[room][panel] @@ -502,15 +509,17 @@ def create_panel_hunt_events(self, world: "LingoWorld"): unhindered_panels_by_color: dict[Optional[str], int] = {} for panel_name, panel_data in room_data.items(): - # We won't count non-counting panels. THE MASTER has special access rules and is handled separately. - if panel_data.non_counting or panel_name == "THE MASTER": + # We won't count non-counting panels. + if panel_data.non_counting: continue # We won't coalesce any panels that have requirements beyond colors. To simplify things for now, we will - # only coalesce single-color panels. Chains/stacks/combo puzzles will be separate. + # only coalesce single-color panels. Chains/stacks/combo puzzles will be separate. THE MASTER has + # special access rules and is handled separately. if len(panel_data.required_panels) > 0 or len(panel_data.required_doors) > 0\ or len(panel_data.required_rooms) > 0\ - or (world.options.shuffle_colors and len(panel_data.colors) > 1): + or (world.options.shuffle_colors and len(panel_data.colors) > 1)\ + or panel_name == "THE MASTER": self.counting_panel_reqs.setdefault(room_name, []).append( (self.calculate_panel_requirements(room_name, panel_name, world), 1)) else: diff --git a/worlds/lingo/regions.py b/worlds/lingo/regions.py index 4b357db261b4..9834f04f9de7 100644 --- a/worlds/lingo/regions.py +++ b/worlds/lingo/regions.py @@ -49,8 +49,15 @@ def connect_entrance(regions: Dict[str, Region], source_region: Region, target_r if door is not None: effective_room = target_region.name if door.room is None else door.room if door.door not in world.player_logic.item_by_door.get(effective_room, {}): - for region in world.player_logic.calculate_door_requirements(effective_room, door.door, world).rooms: + access_reqs = world.player_logic.calculate_door_requirements(effective_room, door.door, world) + for region in access_reqs.rooms: world.multiworld.register_indirect_condition(regions[region], connection) + + # This pretty much only applies to Orange Tower Sixth Floor -> Orange Tower Basement. + if access_reqs.the_master: + for mastery_req in world.player_logic.mastery_reqs: + for region in mastery_req.rooms: + world.multiworld.register_indirect_condition(regions[region], connection) if not pilgrimage and world.options.enable_pilgrimage and is_acceptable_pilgrimage_entrance(entrance_type, world)\ and source_region.name != "Menu": diff --git a/worlds/lingo/rules.py b/worlds/lingo/rules.py index 9cc11fdaea31..d91c53f05b47 100644 --- a/worlds/lingo/rules.py +++ b/worlds/lingo/rules.py @@ -42,12 +42,6 @@ def lingo_can_use_level_2_location(state: CollectionState, world: "LingoWorld"): counted_panels += panel_count if counted_panels >= world.options.level_2_requirement.value - 1: return True - # THE MASTER has to be handled separately, because it has special access rules. - if state.can_reach("Orange Tower Seventh Floor", "Region", world.player)\ - and lingo_can_use_mastery_location(state, world): - counted_panels += 1 - if counted_panels >= world.options.level_2_requirement.value - 1: - return True return False @@ -65,6 +59,9 @@ def _lingo_can_satisfy_requirements(state: CollectionState, access: AccessRequir if not state.has(color.capitalize(), world.player): return False + if access.the_master and not lingo_can_use_mastery_location(state, world): + return False + return True diff --git a/worlds/lingo/test/TestMastery.py b/worlds/lingo/test/TestMastery.py index 3fb3c95a0208..6e563393cf7f 100644 --- a/worlds/lingo/test/TestMastery.py +++ b/worlds/lingo/test/TestMastery.py @@ -36,4 +36,21 @@ def test_requirement(self): self.assertFalse(self.can_reach_location("Orange Tower Seventh Floor - Mastery Achievements")) self.collect_by_name(["Green", "Gray", "Brown", "Yellow"]) - self.assertTrue(self.can_reach_location("Orange Tower Seventh Floor - Mastery Achievements")) \ No newline at end of file + self.assertTrue(self.can_reach_location("Orange Tower Seventh Floor - Mastery Achievements")) + + +class TestMasteryBlocksDependents(LingoTestBase): + options = { + "mastery_achievements": "24", + "shuffle_colors": "true", + "location_checks": "insanity" + } + + def test_requirement(self): + self.collect_all_but("Gray") + self.assertFalse(self.can_reach_location("Orange Tower Basement - THE LIBRARY")) + self.assertFalse(self.can_reach_location("Orange Tower Seventh Floor - MASTERY")) + + self.collect_by_name("Gray") + self.assertTrue(self.can_reach_location("Orange Tower Basement - THE LIBRARY")) + self.assertTrue(self.can_reach_location("Orange Tower Seventh Floor - MASTERY")) From c7eef13b335204f750759e75a43034400ec7cc82 Mon Sep 17 00:00:00 2001 From: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> Date: Mon, 3 Jun 2024 10:36:51 -0400 Subject: [PATCH 63/64] Accounting for name change (#3449) --- worlds/lingo/test/TestMastery.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/worlds/lingo/test/TestMastery.py b/worlds/lingo/test/TestMastery.py index 6e563393cf7f..3ebe40aa22d7 100644 --- a/worlds/lingo/test/TestMastery.py +++ b/worlds/lingo/test/TestMastery.py @@ -49,8 +49,8 @@ class TestMasteryBlocksDependents(LingoTestBase): def test_requirement(self): self.collect_all_but("Gray") self.assertFalse(self.can_reach_location("Orange Tower Basement - THE LIBRARY")) - self.assertFalse(self.can_reach_location("Orange Tower Seventh Floor - MASTERY")) + self.assertFalse(self.can_reach_location("The Fearless - MASTERY")) self.collect_by_name("Gray") self.assertTrue(self.can_reach_location("Orange Tower Basement - THE LIBRARY")) - self.assertTrue(self.can_reach_location("Orange Tower Seventh Floor - MASTERY")) + self.assertTrue(self.can_reach_location("The Fearless - MASTERY")) From 06e65c1dc6ce4a1564d1b6924b83a0c9546011ec Mon Sep 17 00:00:00 2001 From: Chris Wilson Date: Mon, 3 Jun 2024 18:43:01 -0400 Subject: [PATCH 64/64] WebHost: weighted-options bugfixes (#3448) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Fix improper css for word-break on player-options page * Add default handling to weighted-options types * Remove random-low/mid/high from Toggle, Choice, and TextChoice, * Port key sorting for OptionList and OptionSet from player-options to weighted-options * Ensure Choice and TextChoice values are set properly * Remove debug line 🤦‍♂️ --- .../styles/playerOptions/playerOptions.css | 2 +- .../styles/playerOptions/playerOptions.scss | 2 +- .../templates/weightedOptions/macros.html | 35 +++++++++++-------- 3 files changed, 23 insertions(+), 16 deletions(-) diff --git a/WebHostLib/static/styles/playerOptions/playerOptions.css b/WebHostLib/static/styles/playerOptions/playerOptions.css index 6165e3a0f622..56c9263d3330 100644 --- a/WebHostLib/static/styles/playerOptions/playerOptions.css +++ b/WebHostLib/static/styles/playerOptions/playerOptions.css @@ -15,7 +15,7 @@ html { border-radius: 8px; padding: 1rem; color: #eeffeb; - word-break: break-all; + word-break: break-word; } #player-options #player-options-header h1 { margin-bottom: 0; diff --git a/WebHostLib/static/styles/playerOptions/playerOptions.scss b/WebHostLib/static/styles/playerOptions/playerOptions.scss index 525b8ef15403..06bde759d263 100644 --- a/WebHostLib/static/styles/playerOptions/playerOptions.scss +++ b/WebHostLib/static/styles/playerOptions/playerOptions.scss @@ -16,7 +16,7 @@ html{ border-radius: 8px; padding: 1rem; color: #eeffeb; - word-break: break-all; + word-break: break-word; #player-options-header{ h1{ diff --git a/WebHostLib/templates/weightedOptions/macros.html b/WebHostLib/templates/weightedOptions/macros.html index 5b8944a43887..a6e4545fdaf7 100644 --- a/WebHostLib/templates/weightedOptions/macros.html +++ b/WebHostLib/templates/weightedOptions/macros.html @@ -1,9 +1,9 @@ {% macro Toggle(option_name, option) %} - {{ RangeRow(option_name, option, "No", "false") }} - {{ RangeRow(option_name, option, "Yes", "true") }} - {{ RandomRows(option_name, option) }} + {{ RangeRow(option_name, option, "No", "false", False, "true" if option.default else "false") }} + {{ RangeRow(option_name, option, "Yes", "true", False, "true" if option.default else "false") }} + {{ RandomRow(option_name, option) }}
{% endmacro %} @@ -18,10 +18,10 @@ {% for id, name in option.name_lookup.items() %} {% if name != 'random' %} - {{ RangeRow(option_name, option, option.get_option_name(id), name) }} + {{ RangeRow(option_name, option, option.get_option_name(id), name, False, name if option.get_option_name(option.default)|lower == name|lower else None) }} {% endif %} {% endfor %} - {{ RandomRows(option_name, option) }} + {{ RandomRow(option_name, option) }} {% endmacro %} @@ -72,7 +72,9 @@
- + {% if option.default %} + {{ RangeRow(option_name, option, option.default, option.default) }} + {% endif %}
@@ -90,10 +92,10 @@ {% for id, name in option.name_lookup.items() %} {% if name != 'random' %} - {{ RangeRow(option_name, option, option.get_option_name(id), name) }} + {{ RangeRow(option_name, option, option.get_option_name(id), name, False, name if option.get_option_name(option.default)|lower == name else None) }} {% endif %} {% endfor %} - {{ RandomRows(option_name, option) }} + {{ RandomRow(option_name, option) }} {% endmacro %} @@ -112,7 +114,7 @@ type="number" id="{{ option_name }}-{{ item_name }}-qty" name="{{ option_name }}||{{ item_name }}" - value="0" + value="{{ option.default[item_name] if item_name in option.default else "0" }}" />
{% endfor %} @@ -121,13 +123,14 @@ {% macro OptionList(option_name, option) %}
- {% for key in option.valid_keys|sort %} + {% for key in (option.valid_keys if option.valid_keys is ordered else option.valid_keys|sort) %}