From 0ea20f39290c2c86cd9dbe70bedf7d2c8c1016f1 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Wed, 22 May 2024 14:02:18 +0200 Subject: [PATCH 01/37] Core: add panic_method setting (#3261) --- Fill.py | 64 ++++++++++++++++++++++++++++++++++++++++------------- Main.py | 4 ++-- settings.py | 9 ++++++++ 3 files changed, 60 insertions(+), 17 deletions(-) diff --git a/Fill.py b/Fill.py index d9919c133847..d8147b2eac80 100644 --- a/Fill.py +++ b/Fill.py @@ -35,8 +35,8 @@ def fill_restrictive(multiworld: MultiWorld, base_state: CollectionState, locati """ :param multiworld: Multiworld to be filled. :param base_state: State assumed before fill. - :param locations: Locations to be filled with item_pool - :param item_pool: Items to fill into the locations + :param locations: Locations to be filled with item_pool, gets mutated by removing locations that get filled. + :param item_pool: Items to fill into the locations, gets mutated by removing items that get placed. :param single_player_placement: if true, can speed up placement if everything belongs to a single player :param lock: locations are set to locked as they are filled :param swap: if true, swaps of already place items are done in the event of a dead end @@ -220,7 +220,8 @@ def fill_restrictive(multiworld: MultiWorld, base_state: CollectionState, locati def remaining_fill(multiworld: MultiWorld, locations: typing.List[Location], itempool: typing.List[Item], - name: str = "Remaining") -> None: + name: str = "Remaining", + move_unplaceable_to_start_inventory: bool = False) -> None: unplaced_items: typing.List[Item] = [] placements: typing.List[Location] = [] swapped_items: typing.Counter[typing.Tuple[int, str]] = Counter() @@ -284,13 +285,21 @@ def remaining_fill(multiworld: MultiWorld, if unplaced_items and locations: # There are leftover unplaceable items and locations that won't accept them - raise FillError(f"No more spots to place {len(unplaced_items)} items. Remaining locations are invalid.\n" - f"Unplaced items:\n" - f"{', '.join(str(item) for item in unplaced_items)}\n" - f"Unfilled locations:\n" - f"{', '.join(str(location) for location in locations)}\n" - f"Already placed {len(placements)}:\n" - f"{', '.join(str(place) for place in placements)}") + if move_unplaceable_to_start_inventory: + last_batch = [] + for item in unplaced_items: + logging.debug(f"Moved {item} to start_inventory to prevent fill failure.") + multiworld.push_precollected(item) + last_batch.append(multiworld.worlds[item.player].create_filler()) + remaining_fill(multiworld, locations, unplaced_items, name + " Start Inventory Retry") + else: + raise FillError(f"No more spots to place {len(unplaced_items)} items. Remaining locations are invalid.\n" + f"Unplaced items:\n" + f"{', '.join(str(item) for item in unplaced_items)}\n" + f"Unfilled locations:\n" + f"{', '.join(str(location) for location in locations)}\n" + f"Already placed {len(placements)}:\n" + f"{', '.join(str(place) for place in placements)}") itempool.extend(unplaced_items) @@ -420,7 +429,8 @@ def distribute_early_items(multiworld: MultiWorld, return fill_locations, itempool -def distribute_items_restrictive(multiworld: MultiWorld) -> None: +def distribute_items_restrictive(multiworld: MultiWorld, + panic_method: typing.Literal["swap", "raise", "start_inventory"] = "swap") -> None: fill_locations = sorted(multiworld.get_unfilled_locations()) multiworld.random.shuffle(fill_locations) # get items to distribute @@ -470,8 +480,29 @@ def mark_for_locking(location: Location): if progitempool: # "advancement/progression fill" - fill_restrictive(multiworld, multiworld.state, defaultlocations, progitempool, single_player_placement=multiworld.players == 1, - name="Progression") + if panic_method == "swap": + fill_restrictive(multiworld, multiworld.state, defaultlocations, progitempool, + swap=True, + on_place=mark_for_locking, name="Progression", single_player_placement=multiworld.players == 1) + elif panic_method == "raise": + fill_restrictive(multiworld, multiworld.state, defaultlocations, progitempool, + swap=False, + on_place=mark_for_locking, name="Progression", single_player_placement=multiworld.players == 1) + elif panic_method == "start_inventory": + fill_restrictive(multiworld, multiworld.state, defaultlocations, progitempool, + swap=False, allow_partial=True, + on_place=mark_for_locking, name="Progression", single_player_placement=multiworld.players == 1) + if progitempool: + for item in progitempool: + logging.debug(f"Moved {item} to start_inventory to prevent fill failure.") + multiworld.push_precollected(item) + filleritempool.append(multiworld.worlds[item.player].create_filler()) + logging.warning(f"{len(progitempool)} items moved to start inventory," + f" due to failure in Progression fill step.") + progitempool[:] = [] + + else: + raise ValueError(f"Generator Panic Method {panic_method} not recognized.") if progitempool: raise FillError( f"Not enough locations for progression items. " @@ -486,7 +517,9 @@ def mark_for_locking(location: Location): inaccessible_location_rules(multiworld, multiworld.state, defaultlocations) - remaining_fill(multiworld, excludedlocations, filleritempool, "Remaining Excluded") + remaining_fill(multiworld, excludedlocations, filleritempool, "Remaining Excluded", + move_unplaceable_to_start_inventory=panic_method=="start_inventory") + if excludedlocations: raise FillError( f"Not enough filler items for excluded locations. " @@ -495,7 +528,8 @@ def mark_for_locking(location: Location): restitempool = filleritempool + usefulitempool - remaining_fill(multiworld, defaultlocations, restitempool) + remaining_fill(multiworld, defaultlocations, restitempool, + move_unplaceable_to_start_inventory=panic_method=="start_inventory") unplaced = restitempool unfilled = defaultlocations diff --git a/Main.py b/Main.py index 1be91a8bb2f1..8b15a57a69e5 100644 --- a/Main.py +++ b/Main.py @@ -13,7 +13,7 @@ from BaseClasses import CollectionState, Item, Location, LocationProgressType, MultiWorld, Region from Fill import balance_multiworld_progression, distribute_items_restrictive, distribute_planned, flood_items from Options import StartInventoryPool -from Utils import __version__, output_path, version_tuple +from Utils import __version__, output_path, version_tuple, get_settings from settings import get_settings from worlds import AutoWorld from worlds.generic.Rules import exclusion_rules, locality_rules @@ -272,7 +272,7 @@ def find_common_pool(players: Set[int], shared_pool: Set[str]) -> Tuple[ if multiworld.algorithm == 'flood': flood_items(multiworld) # different algo, biased towards early game progress items elif multiworld.algorithm == 'balanced': - distribute_items_restrictive(multiworld) + distribute_items_restrictive(multiworld, get_settings().generator.panic_method) AutoWorld.call_all(multiworld, 'post_fill') diff --git a/settings.py b/settings.py index b463c5a0476c..9d1c0904ddd8 100644 --- a/settings.py +++ b/settings.py @@ -665,6 +665,14 @@ class Race(IntEnum): OFF = 0 ON = 1 + class PanicMethod(str): + """ + What to do if the current item placements appear unsolvable. + raise -> Raise an exception and abort. + swap -> Attempt to fix it by swapping prior placements around. (Default) + start_inventory -> Move remaining items to start_inventory, generate additional filler items to fill locations. + """ + enemizer_path: EnemizerPath = EnemizerPath("EnemizerCLI/EnemizerCLI.Core") # + ".exe" is implied on Windows player_files_path: PlayerFilesPath = PlayerFilesPath("Players") players: Players = Players(0) @@ -673,6 +681,7 @@ class Race(IntEnum): spoiler: Spoiler = Spoiler(3) race: Race = Race(0) plando_options: PlandoOptions = PlandoOptions("bosses, connections, texts") + panic_method: PanicMethod = PanicMethod("swap") class SNIOptions(Group): 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 02/37] 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 03/37] 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 04/37] 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 05/37] 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 06/37] 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 07/37] 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 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�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 08/37] 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 09/37] 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 10/37] 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 11/37] 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 12/37] 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 13/37] 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 14/37] 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 15/37] 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 16/37] 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 17/37] 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 18/37] 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 19/37] 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 20/37] 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 21/37] 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 22/37] 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 23/37] 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 24/37] 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 25/37] 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 26/37] 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 27/37] 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 28/37] 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 29/37] 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 30/37] 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 31/37] 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 32/37] 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 33/37] 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 34/37] 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 35/37] 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 36/37] 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 37/37] 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"