From 5b34e06c8ba39cd04977e4022871cc5699303561 Mon Sep 17 00:00:00 2001 From: qwint Date: Tue, 28 May 2024 20:37:44 -0500 Subject: [PATCH 01/27] 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 02/27] 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 03/27] 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 04/27] 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 05/27] 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 06/27] 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 07/27] 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 08/27] 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 09/27] Hollow Knight: Add missing comma (#3403) --- worlds/hk/Options.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worlds/hk/Options.py b/worlds/hk/Options.py index f7b4420c7447..f408528821cc 100644 --- a/worlds/hk/Options.py +++ b/worlds/hk/Options.py @@ -105,7 +105,7 @@ "RandomizeVesselFragments", "RandomizeCharmNotches", "RandomizePaleOre", - "RandomizeRancidEggs" + "RandomizeRancidEggs", "RandomizeRelics", "RandomizeStags", "RandomizeLifebloodCocoons" From b055a394547a1180d1cadd370b4cd072455ae9e8 Mon Sep 17 00:00:00 2001 From: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> Date: Fri, 31 May 2024 15:48:21 -0400 Subject: [PATCH 10/27] PKMN R/B: "J.r" -> "Jr." (#3423) --- worlds/pokemon_rb/locations.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worlds/pokemon_rb/locations.py b/worlds/pokemon_rb/locations.py index b7b7e533a5ee..251beb59cc18 100644 --- a/worlds/pokemon_rb/locations.py +++ b/worlds/pokemon_rb/locations.py @@ -636,7 +636,7 @@ def __init__(self, flag): LocationData("Rock Tunnel B1F-W", "PokeManiac 3", None, rom_addresses["Trainersanity_EVENT_BEAT_ROCK_TUNNEL_2_TRAINER_2_ITEM"], EventFlag(11), inclusion=trainersanity), LocationData("Route 10-N", "Jr. Trainer F 1", None, rom_addresses["Trainersanity_EVENT_BEAT_ROUTE_10_TRAINER_3_ITEM"], EventFlag(308), inclusion=trainersanity), LocationData("Route 10-C", "PokeManiac 1", None, rom_addresses["Trainersanity_EVENT_BEAT_ROUTE_10_TRAINER_0_ITEM"], EventFlag(311), inclusion=trainersanity), - LocationData("Route 10-S", "J.r Trainer F 2", None, rom_addresses["Trainersanity_EVENT_BEAT_ROUTE_10_TRAINER_5_ITEM"], EventFlag(306), inclusion=trainersanity), + LocationData("Route 10-S", "Jr. Trainer F 2", None, rom_addresses["Trainersanity_EVENT_BEAT_ROUTE_10_TRAINER_5_ITEM"], EventFlag(306), inclusion=trainersanity), LocationData("Route 10-S", "Hiker 1", None, rom_addresses["Trainersanity_EVENT_BEAT_ROUTE_10_TRAINER_1_ITEM"], EventFlag(310), inclusion=trainersanity), LocationData("Route 10-S", "Hiker 2", None, rom_addresses["Trainersanity_EVENT_BEAT_ROUTE_10_TRAINER_4_ITEM"], EventFlag(307), inclusion=trainersanity), LocationData("Route 10-S", "PokeManiac 2", None, rom_addresses["Trainersanity_EVENT_BEAT_ROUTE_10_TRAINER_2_ITEM"], EventFlag(309), inclusion=trainersanity), From 15e06e1779ed97ec2609933bea46da91e97a5679 Mon Sep 17 00:00:00 2001 From: Chris Wilson Date: Fri, 31 May 2024 22:41:03 -0400 Subject: [PATCH 11/27] Fix TextChoice options sometimes creating a broken YAML (#3390) * Fix TextChoice options with custom values improperly being included in YAML output * Update WebHostLib/options.py Co-authored-by: Fabian Dill --------- Co-authored-by: Fabian Dill --- WebHostLib/options.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/WebHostLib/options.py b/WebHostLib/options.py index 1026d7638502..b112c8e361b1 100644 --- a/WebHostLib/options.py +++ b/WebHostLib/options.py @@ -169,9 +169,9 @@ def generate_yaml(game: str): else: options[key] = val - # Detect and build ItemDict options from their name pattern for key, val in options.copy().items(): key_parts = key.rsplit("||", 2) + # Detect and build ItemDict options from their name pattern if key_parts[-1] == "qty": if key_parts[0] not in options: options[key_parts[0]] = {} @@ -179,6 +179,13 @@ def generate_yaml(game: str): options[key_parts[0]][key_parts[1]] = int(val) del options[key] + # Detect keys which end with -custom, indicating a TextChoice with a possible custom value + elif key_parts[-1].endswith("-custom"): + if val: + options[key_parts[-1][:-7]] = val + + del options[key] + # Detect random-* keys and set their options accordingly for key, val in options.copy().items(): if key.startswith("random-"): From f3003ff147b38f696c62ac7cc26d96d5bd6c51bc Mon Sep 17 00:00:00 2001 From: Chris Wilson Date: Fri, 31 May 2024 22:41:49 -0400 Subject: [PATCH 12/27] Fix options pages sometimes displaying blank values in form fields (#3364) --- WebHostLib/options.py | 30 +++++++++++++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/WebHostLib/options.py b/WebHostLib/options.py index b112c8e361b1..62ba86a56626 100644 --- a/WebHostLib/options.py +++ b/WebHostLib/options.py @@ -76,6 +76,34 @@ def test_ordered(obj): def option_presets(game: str) -> Response: world = AutoWorldRegister.world_types[game] + presets = {} + for preset_name, preset in world.web.options_presets.items(): + presets[preset_name] = {} + for preset_option_name, preset_option in preset.items(): + if preset_option == "random": + presets[preset_name][preset_option_name] = preset_option + continue + + option = world.options_dataclass.type_hints[preset_option_name].from_any(preset_option) + if isinstance(option, Options.NamedRange) and isinstance(preset_option, str): + assert preset_option in option.special_range_names, \ + f"Invalid preset value '{preset_option}' for '{preset_option_name}' in '{preset_name}'. " \ + f"Expected {option.special_range_names.keys()} or {option.range_start}-{option.range_end}." + + presets[preset_name][preset_option_name] = option.value + elif isinstance(option, Options.Range): + presets[preset_name][preset_option_name] = option.value + elif isinstance(preset_option, str): + # Ensure the option value is valid for Choice and Toggle options + assert option.name_lookup[option.value] == preset_option, \ + f"Invalid option value '{preset_option}' for '{preset_option_name}' in preset '{preset_name}'. " \ + f"Values must not be resolved to a different option via option.from_text (or an alias)." + # Use the name of the option + presets[preset_name][preset_option_name] = option.current_key + else: + # Use the name of the option + presets[preset_name][preset_option_name] = option.current_key + class SetEncoder(json.JSONEncoder): def default(self, obj): from collections.abc import Set @@ -83,7 +111,7 @@ def default(self, obj): return list(obj) return json.JSONEncoder.default(self, obj) - json_data = json.dumps(world.web.options_presets, cls=SetEncoder) + json_data = json.dumps(presets, cls=SetEncoder) response = Response(json_data) response.headers["Content-Type"] = "application/json" return response From 5aa6ad63ca9da9aa8ea40831522e1d89726c6ce0 Mon Sep 17 00:00:00 2001 From: Zach Parks Date: Sat, 1 Jun 2024 06:07:13 -0500 Subject: [PATCH 13/27] Core: Remove Universally Unique ID Requirements (Per-Game Data Packages) (#1933) --- AdventureClient.py | 2 +- CommonClient.py | 91 ++++++++++++-- MultiServer.py | 50 ++++---- NetUtils.py | 6 +- UndertaleClient.py | 4 +- Utils.py | 5 +- WargrooveClient.py | 6 +- WebHostLib/api/__init__.py | 9 -- WebHostLib/templates/ootTracker.html | 180 --------------------------- Zelda1Client.py | 4 +- docs/network protocol.md | 58 ++++++--- kvui.py | 14 ++- test/general/test_ids.py | 16 --- test/programs/test_common_client.py | 106 ++++++++++++++++ worlds/AutoWorld.py | 13 -- worlds/__init__.py | 1 - worlds/adventure/__init__.py | 1 - worlds/alttp/Client.py | 6 +- worlds/alttp/__init__.py | 1 - worlds/bk_sudoku/__init__.py | 1 - worlds/blasphemous/__init__.py | 1 - worlds/bumpstik/__init__.py | 2 - worlds/checksfinder/__init__.py | 2 - worlds/clique/__init__.py | 1 - worlds/cv64/__init__.py | 1 - worlds/cv64/client.py | 2 +- worlds/dark_souls_3/__init__.py | 1 - worlds/dkc3/Client.py | 6 +- worlds/dkc3/__init__.py | 1 - worlds/dlcquest/__init__.py | 2 - worlds/doom_1993/__init__.py | 1 - worlds/doom_ii/__init__.py | 1 - worlds/factorio/Client.py | 4 +- worlds/factorio/__init__.py | 1 - worlds/ff1/__init__.py | 1 - worlds/ffmq/__init__.py | 3 - worlds/generic/__init__.py | 1 - worlds/heretic/__init__.py | 1 - worlds/hk/__init__.py | 1 - worlds/hylics2/__init__.py | 2 - worlds/kdl3/Client.py | 6 +- worlds/ladx/__init__.py | 5 - worlds/lingo/__init__.py | 1 - worlds/lufia2ac/Client.py | 4 +- worlds/lufia2ac/__init__.py | 1 - worlds/meritous/__init__.py | 2 - worlds/minecraft/__init__.py | 2 - worlds/mmbn3/__init__.py | 2 - worlds/noita/__init__.py | 3 +- worlds/oot/__init__.py | 2 - worlds/overcooked2/__init__.py | 1 - worlds/pokemon_emerald/__init__.py | 1 - worlds/pokemon_rb/__init__.py | 1 - worlds/raft/__init__.py | 1 - worlds/rogue_legacy/__init__.py | 1 - worlds/ror2/__init__.py | 1 - worlds/sa2b/__init__.py | 1 - worlds/sc2/Client.py | 6 +- worlds/sc2/ClientGui.py | 2 +- worlds/shorthike/__init__.py | 1 - worlds/sm/Client.py | 7 +- worlds/sm/__init__.py | 1 - worlds/sm64ex/__init__.py | 1 - worlds/smw/Client.py | 13 +- worlds/smz3/Client.py | 7 +- worlds/smz3/__init__.py | 1 - worlds/soe/__init__.py | 1 - worlds/spire/__init__.py | 1 - worlds/stardew_valley/__init__.py | 1 - worlds/subnautica/__init__.py | 1 - worlds/terraria/__init__.py | 5 - worlds/timespinner/__init__.py | 3 +- worlds/tloz/__init__.py | 1 - worlds/undertale/__init__.py | 2 - worlds/v6/__init__.py | 2 - worlds/yoshisisland/Client.py | 6 +- worlds/zillion/__init__.py | 5 - 77 files changed, 319 insertions(+), 392 deletions(-) delete mode 100644 WebHostLib/templates/ootTracker.html create mode 100644 test/programs/test_common_client.py diff --git a/AdventureClient.py b/AdventureClient.py index 06e4d60dad43..7bfbd5ef6bd3 100644 --- a/AdventureClient.py +++ b/AdventureClient.py @@ -112,7 +112,7 @@ def on_package(self, cmd: str, args: dict): if ': !' not in msg: self._set_message(msg, SYSTEM_MESSAGE_ID) elif cmd == "ReceivedItems": - msg = f"Received {', '.join([self.item_names[item.item] for item in args['items']])}" + msg = f"Received {', '.join([self.item_names.lookup_in_slot(item.item) for item in args['items']])}" self._set_message(msg, SYSTEM_MESSAGE_ID) elif cmd == "Retrieved": if f"adventure_{self.auth}_freeincarnates_used" in args["keys"]: diff --git a/CommonClient.py b/CommonClient.py index 63cac098e22a..8af822cba571 100644 --- a/CommonClient.py +++ b/CommonClient.py @@ -1,5 +1,6 @@ from __future__ import annotations +import collections import copy import logging import asyncio @@ -8,6 +9,7 @@ import typing import time import functools +import warnings import ModuleUpdate ModuleUpdate.update() @@ -173,10 +175,74 @@ class CommonContext: items_handling: typing.Optional[int] = None want_slot_data: bool = True # should slot_data be retrieved via Connect - # data package - # Contents in flux until connection to server is made, to download correct data for this multiworld. - item_names: typing.Dict[int, str] = Utils.KeyedDefaultDict(lambda code: f'Unknown item (ID:{code})') - location_names: typing.Dict[int, str] = Utils.KeyedDefaultDict(lambda code: f'Unknown location (ID:{code})') + class NameLookupDict: + """A specialized dict, with helper methods, for id -> name item/location data package lookups by game.""" + def __init__(self, ctx: CommonContext, lookup_type: typing.Literal["item", "location"]): + self.ctx: CommonContext = ctx + self.lookup_type: typing.Literal["item", "location"] = lookup_type + self._unknown_item: typing.Callable[[int], str] = lambda key: f"Unknown {lookup_type} (ID: {key})" + self._archipelago_lookup: typing.Dict[int, str] = {} + self._flat_store: typing.Dict[int, str] = Utils.KeyedDefaultDict(self._unknown_item) + self._game_store: typing.Dict[str, typing.ChainMap[int, str]] = collections.defaultdict( + lambda: collections.ChainMap(self._archipelago_lookup, Utils.KeyedDefaultDict(self._unknown_item))) + self.warned: bool = False + + # noinspection PyTypeChecker + def __getitem__(self, key: str) -> typing.Mapping[int, str]: + # TODO: In a future version (0.6.0?) this should be simplified by removing implicit id lookups support. + if isinstance(key, int): + if not self.warned: + # Use warnings instead of logger to avoid deprecation message from appearing on user side. + self.warned = True + warnings.warn(f"Implicit name lookup by id only is deprecated and only supported to maintain " + f"backwards compatibility for now. If multiple games share the same id for a " + f"{self.lookup_type}, name could be incorrect. Please use " + f"`{self.lookup_type}_names.lookup_in_game()` or " + f"`{self.lookup_type}_names.lookup_in_slot()` instead.") + return self._flat_store[key] # type: ignore + + return self._game_store[key] + + def __len__(self) -> int: + return len(self._game_store) + + def __iter__(self) -> typing.Iterator[str]: + return iter(self._game_store) + + def __repr__(self) -> str: + return self._game_store.__repr__() + + def lookup_in_game(self, code: int, game_name: typing.Optional[str] = None) -> str: + """Returns the name for an item/location id in the context of a specific game or own game if `game` is + omitted. + """ + if game_name is None: + game_name = self.ctx.game + assert game_name is not None, f"Attempted to lookup {self.lookup_type} with no game name available." + + return self._game_store[game_name][code] + + def lookup_in_slot(self, code: int, slot: typing.Optional[int] = None) -> str: + """Returns the name for an item/location id in the context of a specific slot or own slot if `slot` is + omitted. + """ + if slot is None: + slot = self.ctx.slot + assert slot is not None, f"Attempted to lookup {self.lookup_type} with no slot info available." + + return self.lookup_in_game(code, self.ctx.slot_info[slot].game) + + def update_game(self, game: str, name_to_id_lookup_table: typing.Dict[str, int]) -> None: + """Overrides existing lookup tables for a particular game.""" + id_to_name_lookup_table = Utils.KeyedDefaultDict(self._unknown_item) + id_to_name_lookup_table.update({code: name for name, code in name_to_id_lookup_table.items()}) + self._game_store[game] = collections.ChainMap(self._archipelago_lookup, id_to_name_lookup_table) + self._flat_store.update(id_to_name_lookup_table) # Only needed for legacy lookup method. + if game == "Archipelago": + # Keep track of the Archipelago data package separately so if it gets updated in a custom datapackage, + # it updates in all chain maps automatically. + self._archipelago_lookup.clear() + self._archipelago_lookup.update(id_to_name_lookup_table) # defaults starting_reconnect_delay: int = 5 @@ -231,7 +297,7 @@ class CommonContext: # message box reporting a loss of connection _messagebox_connection_loss: typing.Optional["kvui.MessageBox"] = None - def __init__(self, server_address: typing.Optional[str], password: typing.Optional[str]) -> None: + def __init__(self, server_address: typing.Optional[str] = None, password: typing.Optional[str] = None) -> None: # server state self.server_address = server_address self.username = None @@ -271,6 +337,9 @@ def __init__(self, server_address: typing.Optional[str], password: typing.Option self.exit_event = asyncio.Event() self.watcher_event = asyncio.Event() + self.item_names = self.NameLookupDict(self, "item") + self.location_names = self.NameLookupDict(self, "location") + self.jsontotextparser = JSONtoTextParser(self) self.rawjsontotextparser = RawJSONtoTextParser(self) self.update_data_package(network_data_package) @@ -486,19 +555,17 @@ async def prepare_data_package(self, relevant_games: typing.Set[str], or remote_checksum != cache_checksum: needed_updates.add(game) else: - self.update_game(cached_game) + self.update_game(cached_game, game) if needed_updates: await self.send_msgs([{"cmd": "GetDataPackage", "games": [game_name]} for game_name in needed_updates]) - def update_game(self, game_package: dict): - for item_name, item_id in game_package["item_name_to_id"].items(): - self.item_names[item_id] = item_name - for location_name, location_id in game_package["location_name_to_id"].items(): - self.location_names[location_id] = location_name + def update_game(self, game_package: dict, game: str): + self.item_names.update_game(game, game_package["item_name_to_id"]) + self.location_names.update_game(game, game_package["location_name_to_id"]) def update_data_package(self, data_package: dict): for game, game_data in data_package["games"].items(): - self.update_game(game_data) + self.update_game(game_data, game) def consume_network_data_package(self, data_package: dict): self.update_data_package(data_package) diff --git a/MultiServer.py b/MultiServer.py index 4fb03732d811..2c08b0b4eb08 100644 --- a/MultiServer.py +++ b/MultiServer.py @@ -168,9 +168,11 @@ class Context: slot_info: typing.Dict[int, NetworkSlot] generator_version = Version(0, 0, 0) checksums: typing.Dict[str, str] - item_names: typing.Dict[int, str] = Utils.KeyedDefaultDict(lambda code: f'Unknown item (ID:{code})') + item_names: typing.Dict[str, typing.Dict[int, str]] = ( + collections.defaultdict(lambda: Utils.KeyedDefaultDict(lambda code: f'Unknown item (ID:{code})'))) item_name_groups: typing.Dict[str, typing.Dict[str, typing.Set[str]]] - location_names: typing.Dict[int, str] = Utils.KeyedDefaultDict(lambda code: f'Unknown location (ID:{code})') + location_names: typing.Dict[str, typing.Dict[int, str]] = ( + collections.defaultdict(lambda: Utils.KeyedDefaultDict(lambda code: f'Unknown location (ID:{code})'))) location_name_groups: typing.Dict[str, typing.Dict[str, typing.Set[str]]] all_item_and_group_names: typing.Dict[str, typing.Set[str]] all_location_and_group_names: typing.Dict[str, typing.Set[str]] @@ -271,14 +273,21 @@ def _init_game_data(self): if "checksum" in game_package: self.checksums[game_name] = game_package["checksum"] for item_name, item_id in game_package["item_name_to_id"].items(): - self.item_names[item_id] = item_name + self.item_names[game_name][item_id] = item_name for location_name, location_id in game_package["location_name_to_id"].items(): - self.location_names[location_id] = location_name + self.location_names[game_name][location_id] = location_name self.all_item_and_group_names[game_name] = \ set(game_package["item_name_to_id"]) | set(self.item_name_groups[game_name]) self.all_location_and_group_names[game_name] = \ set(game_package["location_name_to_id"]) | set(self.location_name_groups.get(game_name, [])) + archipelago_item_names = self.item_names["Archipelago"] + archipelago_location_names = self.location_names["Archipelago"] + for game in [game_name for game_name in self.gamespackage if game_name != "Archipelago"]: + # Add Archipelago items and locations to each data package. + self.item_names[game].update(archipelago_item_names) + self.location_names[game].update(archipelago_location_names) + def item_names_for_game(self, game: str) -> typing.Optional[typing.Dict[str, int]]: return self.gamespackage[game]["item_name_to_id"] if game in self.gamespackage else None @@ -783,10 +792,7 @@ async def on_client_connected(ctx: Context, client: Client): for slot, connected_clients in clients.items(): if connected_clients: name = ctx.player_names[team, slot] - players.append( - NetworkPlayer(team, slot, - ctx.name_aliases.get((team, slot), name), name) - ) + players.append(NetworkPlayer(team, slot, ctx.name_aliases.get((team, slot), name), name)) games = {ctx.games[x] for x in range(1, len(ctx.games) + 1)} games.add("Archipelago") await ctx.send_msgs(client, [{ @@ -801,8 +807,6 @@ async def on_client_connected(ctx: Context, client: Client): 'permissions': get_permissions(ctx), 'hint_cost': ctx.hint_cost, 'location_check_points': ctx.location_check_points, - 'datapackage_versions': {game: game_data["version"] for game, game_data - in ctx.gamespackage.items() if game in games}, 'datapackage_checksums': {game: game_data["checksum"] for game, game_data in ctx.gamespackage.items() if game in games and "checksum" in game_data}, 'seed_name': ctx.seed_name, @@ -1006,8 +1010,8 @@ def register_location_checks(ctx: Context, team: int, slot: int, locations: typi send_items_to(ctx, team, target_player, new_item) ctx.logger.info('(Team #%d) %s sent %s to %s (%s)' % ( - team + 1, ctx.player_names[(team, slot)], ctx.item_names[item_id], - ctx.player_names[(team, target_player)], ctx.location_names[location])) + team + 1, ctx.player_names[(team, slot)], ctx.item_names[ctx.slot_info[target_player].game][item_id], + ctx.player_names[(team, target_player)], ctx.location_names[ctx.slot_info[slot].game][location])) info_text = json_format_send_event(new_item, target_player) ctx.broadcast_team(team, [info_text]) @@ -1061,8 +1065,8 @@ def collect_hint_location_id(ctx: Context, team: int, slot: int, seeked_location def format_hint(ctx: Context, team: int, hint: NetUtils.Hint) -> str: text = f"[Hint]: {ctx.player_names[team, hint.receiving_player]}'s " \ - f"{ctx.item_names[hint.item]} is " \ - f"at {ctx.location_names[hint.location]} " \ + f"{ctx.item_names[ctx.slot_info[hint.receiving_player].game][hint.item]} is " \ + f"at {ctx.location_names[ctx.slot_info[hint.finding_player].game][hint.location]} " \ f"in {ctx.player_names[team, hint.finding_player]}'s World" if hint.entrance: @@ -1364,7 +1368,7 @@ def _cmd_remaining(self) -> bool: if self.ctx.remaining_mode == "enabled": remaining_item_ids = get_remaining(self.ctx, self.client.team, self.client.slot) if remaining_item_ids: - self.output("Remaining items: " + ", ".join(self.ctx.item_names[item_id] + self.output("Remaining items: " + ", ".join(self.ctx.item_names[self.client.slot.game][item_id] for item_id in remaining_item_ids)) else: self.output("No remaining items found.") @@ -1377,7 +1381,7 @@ def _cmd_remaining(self) -> bool: if self.ctx.client_game_state[self.client.team, self.client.slot] == ClientStatus.CLIENT_GOAL: remaining_item_ids = get_remaining(self.ctx, self.client.team, self.client.slot) if remaining_item_ids: - self.output("Remaining items: " + ", ".join(self.ctx.item_names[item_id] + self.output("Remaining items: " + ", ".join(self.ctx.item_names[self.client.slot.game][item_id] for item_id in remaining_item_ids)) else: self.output("No remaining items found.") @@ -1395,7 +1399,8 @@ def _cmd_missing(self, filter_text="") -> bool: locations = get_missing_checks(self.ctx, self.client.team, self.client.slot) if locations: - names = [self.ctx.location_names[location] for location in locations] + game = self.ctx.slot_info[self.client.slot].game + names = [self.ctx.location_names[game][location] for location in locations] if filter_text: location_groups = self.ctx.location_name_groups[self.ctx.games[self.client.slot]] if filter_text in location_groups: # location group name @@ -1420,7 +1425,8 @@ def _cmd_checked(self, filter_text="") -> bool: locations = get_checked_checks(self.ctx, self.client.team, self.client.slot) if locations: - names = [self.ctx.location_names[location] for location in locations] + game = self.ctx.slot_info[self.client.slot].game + names = [self.ctx.location_names[game][location] for location in locations] if filter_text: location_groups = self.ctx.location_name_groups[self.ctx.games[self.client.slot]] if filter_text in location_groups: # location group name @@ -1501,10 +1507,10 @@ def get_hints(self, input_text: str, for_location: bool = False) -> bool: elif input_text.isnumeric(): game = self.ctx.games[self.client.slot] hint_id = int(input_text) - hint_name = self.ctx.item_names[hint_id] \ - if not for_location and hint_id in self.ctx.item_names \ - else self.ctx.location_names[hint_id] \ - if for_location and hint_id in self.ctx.location_names \ + hint_name = self.ctx.item_names[game][hint_id] \ + if not for_location and hint_id in self.ctx.item_names[game] \ + else self.ctx.location_names[game][hint_id] \ + if for_location and hint_id in self.ctx.location_names[game] \ else None if hint_name in self.ctx.non_hintable_names[game]: self.output(f"Sorry, \"{hint_name}\" is marked as non-hintable.") diff --git a/NetUtils.py b/NetUtils.py index 8fc3929e60b4..076fdc3ba44f 100644 --- a/NetUtils.py +++ b/NetUtils.py @@ -247,7 +247,7 @@ def _handle_item_name(self, node: JSONMessagePart): def _handle_item_id(self, node: JSONMessagePart): item_id = int(node["text"]) - node["text"] = self.ctx.item_names[item_id] + node["text"] = self.ctx.item_names.lookup_in_slot(item_id, node["player"]) return self._handle_item_name(node) def _handle_location_name(self, node: JSONMessagePart): @@ -255,8 +255,8 @@ def _handle_location_name(self, node: JSONMessagePart): return self._handle_color(node) def _handle_location_id(self, node: JSONMessagePart): - item_id = int(node["text"]) - node["text"] = self.ctx.location_names[item_id] + location_id = int(node["text"]) + node["text"] = self.ctx.location_names.lookup_in_slot(location_id, node["player"]) return self._handle_location_name(node) def _handle_entrance_name(self, node: JSONMessagePart): diff --git a/UndertaleClient.py b/UndertaleClient.py index e1538ce81d2e..cdc21c561ab8 100644 --- a/UndertaleClient.py +++ b/UndertaleClient.py @@ -247,8 +247,8 @@ async def process_undertale_cmd(ctx: UndertaleContext, cmd: str, args: dict): with open(os.path.join(ctx.save_game_folder, filename), "w") as f: toDraw = "" for i in range(20): - if i < len(str(ctx.item_names[l.item])): - toDraw += str(ctx.item_names[l.item])[i] + if i < len(str(ctx.item_names.lookup_in_slot(l.item))): + toDraw += str(ctx.item_names.lookup_in_slot(l.item))[i] else: break f.write(toDraw) diff --git a/Utils.py b/Utils.py index 780271996583..9f6837215c5e 100644 --- a/Utils.py +++ b/Utils.py @@ -46,7 +46,7 @@ def as_simple_string(self) -> str: return ".".join(str(item) for item in self) -__version__ = "0.4.6" +__version__ = "0.5.0" version_tuple = tuplize_version(__version__) is_linux = sys.platform.startswith("linux") @@ -458,6 +458,9 @@ class KeyedDefaultDict(collections.defaultdict): """defaultdict variant that uses the missing key as argument to default_factory""" default_factory: typing.Callable[[typing.Any], typing.Any] + def __init__(self, default_factory: typing.Callable[[Any], Any] = None, **kwargs): + super().__init__(default_factory, **kwargs) + def __missing__(self, key): self[key] = value = self.default_factory(key) return value diff --git a/WargrooveClient.py b/WargrooveClient.py index 77180502cefc..c5fdeb3532f5 100644 --- a/WargrooveClient.py +++ b/WargrooveClient.py @@ -176,7 +176,7 @@ def on_package(self, cmd: str, args: dict): if not os.path.isfile(path): open(path, 'w').close() # Announcing commander unlocks - item_name = self.item_names[network_item.item] + item_name = self.item_names.lookup_in_slot(network_item.item) if item_name in faction_table.keys(): for commander in faction_table[item_name]: logger.info(f"{commander.name} has been unlocked!") @@ -197,7 +197,7 @@ def on_package(self, cmd: str, args: dict): open(print_path, 'w').close() with open(print_path, 'w') as f: f.write("Received " + - self.item_names[network_item.item] + + self.item_names.lookup_in_slot(network_item.item) + " from " + self.player_names[network_item.player]) f.close() @@ -342,7 +342,7 @@ def update_commander_data(self): faction_items = 0 faction_item_names = [faction + ' Commanders' for faction in faction_table.keys()] for network_item in self.items_received: - if self.item_names[network_item.item] in faction_item_names: + if self.item_names.lookup_in_slot(network_item.item) in faction_item_names: faction_items += 1 starting_groove = (faction_items - 1) * self.starting_groove_multiplier # Must be an integer larger than 0 diff --git a/WebHostLib/api/__init__.py b/WebHostLib/api/__init__.py index cfdbe25ff2fe..22d1f19f6bdf 100644 --- a/WebHostLib/api/__init__.py +++ b/WebHostLib/api/__init__.py @@ -56,15 +56,6 @@ def get_datapackage(): return network_data_package -@api_endpoints.route('/datapackage_version') -@cache.cached() -def get_datapackage_versions(): - from worlds import AutoWorldRegister - - version_package = {game: world.data_version for game, world in AutoWorldRegister.world_types.items()} - return version_package - - @api_endpoints.route('/datapackage_checksum') @cache.cached() def get_datapackage_checksums(): diff --git a/WebHostLib/templates/ootTracker.html b/WebHostLib/templates/ootTracker.html deleted file mode 100644 index ea7a6d5a4c30..000000000000 --- a/WebHostLib/templates/ootTracker.html +++ /dev/null @@ -1,180 +0,0 @@ - - - - {{ player_name }}'s Tracker - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-
- -
{{ hookshot_length }}
-
-
-
- -
{{ bottle_count if bottle_count > 0 else '' }}
-
-
-
- -
{{ wallet_size }}
-
-
-
- -
Zelda
-
-
-
- -
Epona
-
-
-
- -
Saria
-
-
-
- -
Sun
-
-
-
- -
Time
-
-
-
- -
Storms
-
-
-
- -
{{ token_count }}
-
-
-
- -
Min
-
-
-
- -
Bol
-
-
-
- -
Ser
-
-
-
- -
Req
-
-
-
- -
Noc
-
-
-
- -
Pre
-
-
-
- -
{{ piece_count if piece_count > 0 else '' }}
-
-
- - - - - - - - {% for area in checks_done %} - - - - - - - - {% for location in location_info[area] %} - - - - - - - {% endfor %} - - {% endfor %} -
Items
{{ area }} {{'▼' if area != 'Total'}}{{ small_key_counts.get(area, '-') }}{{ boss_key_counts.get(area, '-') }}{{ checks_done[area] }} / {{ checks_in_area[area] }}
{{ location }}{{ '✔' if location_info[area][location] else '' }}
-
- - diff --git a/Zelda1Client.py b/Zelda1Client.py index cd76a0a5ca78..6d7af0a94dcf 100644 --- a/Zelda1Client.py +++ b/Zelda1Client.py @@ -152,7 +152,7 @@ def get_payload(ctx: ZeldaContext): def reconcile_shops(ctx: ZeldaContext): - checked_location_names = [ctx.location_names[location] for location in ctx.checked_locations] + checked_location_names = [ctx.location_names.lookup_in_slot(location) for location in ctx.checked_locations] shops = [location for location in checked_location_names if "Shop" in location] left_slots = [shop for shop in shops if "Left" in shop] middle_slots = [shop for shop in shops if "Middle" in shop] @@ -190,7 +190,7 @@ async def parse_locations(locations_array, ctx: ZeldaContext, force: bool, zone= locations_checked = [] location = None for location in ctx.missing_locations: - location_name = ctx.location_names[location] + location_name = ctx.location_names.lookup_in_slot(location) if location_name in Locations.overworld_locations and zone == "overworld": status = locations_array[Locations.major_location_offsets[location_name]] diff --git a/docs/network protocol.md b/docs/network protocol.md index 604ff6708fca..da5c41431501 100644 --- a/docs/network protocol.md +++ b/docs/network protocol.md @@ -53,7 +53,7 @@ Example: ``` ## (Server -> Client) -These packets are are sent from the multiworld server to the client. They are not messages which the server accepts. +These packets are sent from the multiworld server to the client. They are not messages which the server accepts. * [RoomInfo](#RoomInfo) * [ConnectionRefused](#ConnectionRefused) * [Connected](#Connected) @@ -80,7 +80,6 @@ Sent to clients when they connect to an Archipelago server. | hint_cost | int | The percentage of total locations that need to be checked to receive a hint from the server. | | location_check_points | int | The amount of hint points you receive per item/location check completed. | | games | list\[str\] | List of games present in this multiworld. | -| datapackage_versions | dict\[str, int\] | Data versions of the individual games' data packages the server will send. Used to decide which games' caches are outdated. See [Data Package Contents](#Data-Package-Contents). **Deprecated. Use `datapackage_checksums` instead.** | | datapackage_checksums | dict[str, str] | Checksum hash of the individual games' data packages the server will send. Used by newer clients to decide which games' caches are outdated. See [Data Package Contents](#Data-Package-Contents) for more information. | | seed_name | str | Uniquely identifying name of this generation | | time | float | Unix time stamp of "now". Send for time synchronization if wanted for things like the DeathLink Bounce. | @@ -500,9 +499,9 @@ In JSON this may look like: {"item": 3, "location": 3, "player": 3, "flags": 0} ] ``` -`item` is the item id of the item. Item ids are in the range of ± 253-1. +`item` is the item id of the item. Item ids are only supported in the range of [-253, 253 - 1], with anything ≤ 0 reserved for Archipelago use. -`location` is the location id of the item inside the world. Location ids are in the range of ± 253-1. +`location` is the location id of the item inside the world. Location ids are only supported in the range of [-253, 253 - 1], with anything ≤ 0 reserved for Archipelago use. `player` is the player slot of the world the item is located in, except when inside an [LocationInfo](#LocationInfo) Packet then it will be the slot of the player to receive the item @@ -646,15 +645,47 @@ class Hint(typing.NamedTuple): ``` ### Data Package Contents -A data package is a JSON object which may contain arbitrary metadata to enable a client to interact with the Archipelago server most easily. Currently, this package is used to send ID to name mappings so that clients need not maintain their own mappings. - -We encourage clients to cache the data package they receive on disk, or otherwise not tied to a session. You will know when your cache is outdated if the [RoomInfo](#RoomInfo) packet or the datapackage itself denote a different version. A special case is datapackage version 0, where it is expected the package is custom and should not be cached. - -Note: - * Any ID is unique to its type across AP: Item 56 only exists once and Location 56 only exists once. - * Any Name is unique to its type across its own Game only: Single Arrow can exist in two games. - * The IDs from the game "Archipelago" may be used in any other game. - Especially Location ID -1: Cheat Console and -2: Server (typically Remote Start Inventory) +A data package is a JSON object which may contain arbitrary metadata to enable a client to interact with the Archipelago +server most easily and not maintain their own mappings. Some contents include: + + - Name to ID mappings for items and locations. + - A checksum of each game's data package for clients to tell if a cached package is invalid. + +We encourage clients to cache the data package they receive on disk, or otherwise not tied to a session. You will know +when your cache is outdated if the [RoomInfo](#RoomInfo) packet or the datapackage itself denote a different checksum +than any locally cached ones. + +**Important Notes about IDs and Names**: + +* IDs ≤ 0 are reserved for "Archipelago" and should not be used by other world implementations. +* The IDs from the game "Archipelago" (in `worlds/generic`) may be used in any world. + * Especially Location ID `-1`: `Cheat Console` and `-2`: `Server` (typically Remote Start Inventory) +* Any names and IDs are only unique in its own world data package, but different games may reuse these names or IDs. + * At runtime, you will need to look up the game of the player to know which item or location ID/Name to lookup in the + data package. This can be easily achieved by reviewing the `slot_info` for a particular player ID prior to lookup. + * For example, a data package like this is valid (Some properties such as `checksum` were omitted): + ```json + { + "games": { + "Game A": { + "location_name_to_id": { + "Boss Chest": 40 + }, + "item_name_to_id": { + "Item X": 12 + } + }, + "Game B": { + "location_name_to_id": { + "Minigame Prize": 40 + }, + "item_name_to_id": { + "Item X": 40 + } + } + } + } + ``` #### Contents | Name | Type | Notes | @@ -668,7 +699,6 @@ GameData is a **dict** but contains these keys and values. It's broken out into |---------------------|----------------|-------------------------------------------------------------------------------------------------------------------------------| | item_name_to_id | dict[str, int] | Mapping of all item names to their respective ID. | | location_name_to_id | dict[str, int] | Mapping of all location names to their respective ID. | -| version | int | Version number of this game's data. Deprecated. Used by older clients to request an updated datapackage if cache is outdated. | | checksum | str | A checksum hash of this game's data. | ### Tags diff --git a/kvui.py b/kvui.py index a1663126cc71..98aa9516b266 100644 --- a/kvui.py +++ b/kvui.py @@ -683,10 +683,18 @@ def refresh_hints(self, hints): for hint in hints: data.append({ "receiving": {"text": self.parser.handle_node({"type": "player_id", "text": hint["receiving_player"]})}, - "item": {"text": self.parser.handle_node( - {"type": "item_id", "text": hint["item"], "flags": hint["item_flags"]})}, + "item": {"text": self.parser.handle_node({ + "type": "item_id", + "text": hint["item"], + "flags": hint["item_flags"], + "player": hint["receiving_player"], + })}, "finding": {"text": self.parser.handle_node({"type": "player_id", "text": hint["finding_player"]})}, - "location": {"text": self.parser.handle_node({"type": "location_id", "text": hint["location"]})}, + "location": {"text": self.parser.handle_node({ + "type": "location_id", + "text": hint["location"], + "player": hint["finding_player"], + })}, "entrance": {"text": self.parser.handle_node({"type": "color" if hint["entrance"] else "text", "color": "blue", "text": hint["entrance"] if hint["entrance"] else "Vanilla"})}, diff --git a/test/general/test_ids.py b/test/general/test_ids.py index 98c41b67b176..e4010af394f5 100644 --- a/test/general/test_ids.py +++ b/test/general/test_ids.py @@ -6,22 +6,6 @@ class TestIDs(unittest.TestCase): - def test_unique_items(self): - """Tests that every game has a unique ID per item in the datapackage""" - known_item_ids = set() - for gamename, world_type in AutoWorldRegister.world_types.items(): - current = len(known_item_ids) - known_item_ids |= set(world_type.item_id_to_name) - self.assertEqual(len(known_item_ids) - len(world_type.item_id_to_name), current) - - def test_unique_locations(self): - """Tests that every game has a unique ID per location in the datapackage""" - known_location_ids = set() - for gamename, world_type in AutoWorldRegister.world_types.items(): - current = len(known_location_ids) - known_location_ids |= set(world_type.location_id_to_name) - self.assertEqual(len(known_location_ids) - len(world_type.location_id_to_name), current) - def test_range_items(self): """There are Javascript clients, which are limited to Number.MAX_SAFE_INTEGER due to 64bit float precision.""" for gamename, world_type in AutoWorldRegister.world_types.items(): diff --git a/test/programs/test_common_client.py b/test/programs/test_common_client.py new file mode 100644 index 000000000000..9936240d17b9 --- /dev/null +++ b/test/programs/test_common_client.py @@ -0,0 +1,106 @@ +import unittest + +import NetUtils +from CommonClient import CommonContext + + +class TestCommonContext(unittest.IsolatedAsyncioTestCase): + async def asyncSetUp(self): + self.ctx = CommonContext() + self.ctx.slot = 1 # Pretend we're player 1 for this. + self.ctx.slot_info.update({ + 1: NetUtils.NetworkSlot("Player 1", "__TestGame1", NetUtils.SlotType.player), + 2: NetUtils.NetworkSlot("Player 2", "__TestGame1", NetUtils.SlotType.player), + 3: NetUtils.NetworkSlot("Player 3", "__TestGame2", NetUtils.SlotType.player), + }) + self.ctx.consume_players_package([ + NetUtils.NetworkPlayer(1, 1, "Player 1", "Player 1"), + NetUtils.NetworkPlayer(1, 2, "Player 2", "Player 2"), + NetUtils.NetworkPlayer(1, 3, "Player 3", "Player 3"), + ]) + # Using IDs outside the "safe range" for testing purposes only. If this fails unit tests, it's because + # another world is not following the spec for allowed ID ranges. + self.ctx.update_data_package({ + "games": { + "__TestGame1": { + "location_name_to_id": { + "Test Location 1 - Safe": 2**54 + 1, + "Test Location 2 - Duplicate": 2**54 + 2, + }, + "item_name_to_id": { + "Test Item 1 - Safe": 2**54 + 1, + "Test Item 2 - Duplicate": 2**54 + 2, + }, + }, + "__TestGame2": { + "location_name_to_id": { + "Test Location 3 - Duplicate": 2**54 + 2, + }, + "item_name_to_id": { + "Test Item 3 - Duplicate": 2**54 + 2, + }, + }, + }, + }) + + async def test_archipelago_datapackage_lookups_exist(self): + assert "Archipelago" in self.ctx.item_names, "Archipelago item names entry does not exist" + assert "Archipelago" in self.ctx.location_names, "Archipelago location names entry does not exist" + + async def test_implicit_name_lookups(self): + # Items + assert self.ctx.item_names[2**54 + 1] == "Test Item 1 - Safe" + assert self.ctx.item_names[2**54 + 3] == f"Unknown item (ID: {2**54+3})" + assert self.ctx.item_names[-1] == "Nothing" + + # Locations + assert self.ctx.location_names[2**54 + 1] == "Test Location 1 - Safe" + assert self.ctx.location_names[2**54 + 3] == f"Unknown location (ID: {2**54+3})" + assert self.ctx.location_names[-1] == "Cheat Console" + + async def test_explicit_name_lookups(self): + # Items + assert self.ctx.item_names["__TestGame1"][2**54+1] == "Test Item 1 - Safe" + assert self.ctx.item_names["__TestGame1"][2**54+2] == "Test Item 2 - Duplicate" + assert self.ctx.item_names["__TestGame1"][2**54+3] == f"Unknown item (ID: {2**54+3})" + assert self.ctx.item_names["__TestGame1"][-1] == "Nothing" + assert self.ctx.item_names["__TestGame2"][2**54+1] == f"Unknown item (ID: {2**54+1})" + assert self.ctx.item_names["__TestGame2"][2**54+2] == "Test Item 3 - Duplicate" + assert self.ctx.item_names["__TestGame2"][2**54+3] == f"Unknown item (ID: {2**54+3})" + assert self.ctx.item_names["__TestGame2"][-1] == "Nothing" + + # Locations + assert self.ctx.location_names["__TestGame1"][2**54+1] == "Test Location 1 - Safe" + assert self.ctx.location_names["__TestGame1"][2**54+2] == "Test Location 2 - Duplicate" + assert self.ctx.location_names["__TestGame1"][2**54+3] == f"Unknown location (ID: {2**54+3})" + assert self.ctx.location_names["__TestGame1"][-1] == "Cheat Console" + assert self.ctx.location_names["__TestGame2"][2**54+1] == f"Unknown location (ID: {2**54+1})" + assert self.ctx.location_names["__TestGame2"][2**54+2] == "Test Location 3 - Duplicate" + assert self.ctx.location_names["__TestGame2"][2**54+3] == f"Unknown location (ID: {2**54+3})" + assert self.ctx.location_names["__TestGame2"][-1] == "Cheat Console" + + async def test_lookup_helper_functions(self): + # Checking own slot. + assert self.ctx.item_names.lookup_in_slot(2 ** 54 + 1) == "Test Item 1 - Safe" + assert self.ctx.item_names.lookup_in_slot(2 ** 54 + 2) == "Test Item 2 - Duplicate" + assert self.ctx.item_names.lookup_in_slot(2 ** 54 + 3) == f"Unknown item (ID: {2 ** 54 + 3})" + assert self.ctx.item_names.lookup_in_slot(-1) == f"Nothing" + + # Checking others' slots. + assert self.ctx.item_names.lookup_in_slot(2 ** 54 + 1, 2) == "Test Item 1 - Safe" + assert self.ctx.item_names.lookup_in_slot(2 ** 54 + 2, 2) == "Test Item 2 - Duplicate" + assert self.ctx.item_names.lookup_in_slot(2 ** 54 + 1, 3) == f"Unknown item (ID: {2 ** 54 + 1})" + assert self.ctx.item_names.lookup_in_slot(2 ** 54 + 2, 3) == "Test Item 3 - Duplicate" + + # Checking by game. + assert self.ctx.item_names.lookup_in_game(2 ** 54 + 1, "__TestGame1") == "Test Item 1 - Safe" + assert self.ctx.item_names.lookup_in_game(2 ** 54 + 2, "__TestGame1") == "Test Item 2 - Duplicate" + assert self.ctx.item_names.lookup_in_game(2 ** 54 + 3, "__TestGame1") == f"Unknown item (ID: {2 ** 54 + 3})" + assert self.ctx.item_names.lookup_in_game(2 ** 54 + 1, "__TestGame2") == f"Unknown item (ID: {2 ** 54 + 1})" + assert self.ctx.item_names.lookup_in_game(2 ** 54 + 2, "__TestGame2") == "Test Item 3 - Duplicate" + + # Checking with Archipelago ids are valid in any game package. + assert self.ctx.item_names.lookup_in_slot(-1, 2) == "Nothing" + assert self.ctx.item_names.lookup_in_slot(-1, 3) == "Nothing" + assert self.ctx.item_names.lookup_in_game(-1, "__TestGame1") == "Nothing" + assert self.ctx.item_names.lookup_in_game(-1, "__TestGame2") == "Nothing" diff --git a/worlds/AutoWorld.py b/worlds/AutoWorld.py index 5d674c0c22fd..6e17f023f6fb 100644 --- a/worlds/AutoWorld.py +++ b/worlds/AutoWorld.py @@ -258,18 +258,6 @@ class World(metaclass=AutoWorldRegister): location_name_groups: ClassVar[Dict[str, Set[str]]] = {} """maps location group names to sets of locations. Example: {"Sewer": {"Sewer Key Drop 1", "Sewer Key Drop 2"}}""" - data_version: ClassVar[int] = 0 - """ - Increment this every time something in your world's names/id mappings changes. - - When this is set to 0, that world's DataPackage is considered in "testing mode", which signals to servers/clients - that it should not be cached, and clients should request that world's DataPackage every connection. Not - recommended for production-ready worlds. - - Deprecated. Clients should utilize `checksum` to determine if DataPackage has changed since last connection and - request a new DataPackage, if necessary. - """ - required_client_version: Tuple[int, int, int] = (0, 1, 6) """ override this if changes to a world break forward-compatibility of the client @@ -543,7 +531,6 @@ def get_data_package_data(cls) -> "GamesPackage": "item_name_to_id": cls.item_name_to_id, "location_name_groups": sorted_location_name_groups, "location_name_to_id": cls.location_name_to_id, - "version": cls.data_version, } res["checksum"] = data_package_checksum(res) return res diff --git a/worlds/__init__.py b/worlds/__init__.py index 53b0c5ceb948..09f72882195e 100644 --- a/worlds/__init__.py +++ b/worlds/__init__.py @@ -33,7 +33,6 @@ class GamesPackage(TypedDict, total=False): location_name_groups: Dict[str, List[str]] location_name_to_id: Dict[str, int] checksum: str - version: int # TODO: Remove support after per game data packages API change. class DataPackage(TypedDict): diff --git a/worlds/adventure/__init__.py b/worlds/adventure/__init__.py index 84caca828f2c..1c2583b3ed6e 100644 --- a/worlds/adventure/__init__.py +++ b/worlds/adventure/__init__.py @@ -113,7 +113,6 @@ class AdventureWorld(World): settings: ClassVar[AdventureSettings] item_name_to_id: ClassVar[Dict[str, int]] = {name: data.id for name, data in item_table.items()} location_name_to_id: ClassVar[Dict[str, int]] = {name: data.location_id for name, data in location_table.items()} - data_version: ClassVar[int] = 1 required_client_version: Tuple[int, int, int] = (0, 3, 9) def __init__(self, world: MultiWorld, player: int): diff --git a/worlds/alttp/Client.py b/worlds/alttp/Client.py index 5b27f559efd7..db7555f24615 100644 --- a/worlds/alttp/Client.py +++ b/worlds/alttp/Client.py @@ -339,7 +339,7 @@ async def track_locations(ctx, roomid, roomdata) -> bool: def new_check(location_id): new_locations.append(location_id) ctx.locations_checked.add(location_id) - location = ctx.location_names[location_id] + location = ctx.location_names.lookup_in_slot(location_id) snes_logger.info( f'New Check: {location} ' + f'({len(ctx.checked_locations) + 1 if ctx.checked_locations else len(ctx.locations_checked)}/' + @@ -552,9 +552,9 @@ async def game_watcher(self, ctx): item = ctx.items_received[recv_index] recv_index += 1 logging.info('Received %s from %s (%s) (%d/%d in list)' % ( - color(ctx.item_names[item.item], 'red', 'bold'), + color(ctx.item_names.lookup_in_slot(item.item), 'red', 'bold'), color(ctx.player_names[item.player], 'yellow'), - ctx.location_names[item.location], recv_index, len(ctx.items_received))) + ctx.location_names.lookup_in_slot(item.location, item.player), recv_index, len(ctx.items_received))) snes_buffered_write(ctx, RECV_PROGRESS_ADDR, bytes([recv_index & 0xFF, (recv_index >> 8) & 0xFF])) diff --git a/worlds/alttp/__init__.py b/worlds/alttp/__init__.py index ae3dfe9e3b1a..3176f7a7fcce 100644 --- a/worlds/alttp/__init__.py +++ b/worlds/alttp/__init__.py @@ -213,7 +213,6 @@ class ALTTPWorld(World): item_name_to_id = {name: data.item_code for name, data in item_table.items() if type(data.item_code) == int} location_name_to_id = lookup_name_to_id - data_version = 9 required_client_version = (0, 4, 1) web = ALTTPWeb() diff --git a/worlds/bk_sudoku/__init__.py b/worlds/bk_sudoku/__init__.py index 195339c38070..2c57bc7301ff 100644 --- a/worlds/bk_sudoku/__init__.py +++ b/worlds/bk_sudoku/__init__.py @@ -34,7 +34,6 @@ class Bk_SudokuWorld(World): """ game = "Sudoku" web = Bk_SudokuWebWorld() - data_version = 1 item_name_to_id: Dict[str, int] = {} location_name_to_id: Dict[str, int] = {} diff --git a/worlds/blasphemous/__init__.py b/worlds/blasphemous/__init__.py index 9abcd81b20e1..a46fb55b9541 100644 --- a/worlds/blasphemous/__init__.py +++ b/worlds/blasphemous/__init__.py @@ -32,7 +32,6 @@ class BlasphemousWorld(World): game: str = "Blasphemous" web = BlasphemousWeb() - data_version = 2 item_name_to_id = {item["name"]: (base_id + index) for index, item in enumerate(item_table)} location_name_to_id = {loc["name"]: (base_id + index) for index, loc in enumerate(location_table)} diff --git a/worlds/bumpstik/__init__.py b/worlds/bumpstik/__init__.py index d922c0277ac3..fe261dc94d30 100644 --- a/worlds/bumpstik/__init__.py +++ b/worlds/bumpstik/__init__.py @@ -39,8 +39,6 @@ class BumpStikWorld(World): location_name_to_id = location_table item_name_groups = item_groups - data_version = 1 - required_client_version = (0, 3, 8) options: BumpstikOptions diff --git a/worlds/checksfinder/__init__.py b/worlds/checksfinder/__init__.py index b70c65bb08f5..c8b9587f8500 100644 --- a/worlds/checksfinder/__init__.py +++ b/worlds/checksfinder/__init__.py @@ -33,8 +33,6 @@ class ChecksFinderWorld(World): item_name_to_id = {name: data.code for name, data in item_table.items()} location_name_to_id = {name: data.id for name, data in advancement_table.items()} - data_version = 4 - def _get_checksfinder_data(self): return { 'world_seed': self.multiworld.per_slot_randoms[self.player].getrandbits(32), diff --git a/worlds/clique/__init__.py b/worlds/clique/__init__.py index 30c0e47f818e..b5cc74d94ac0 100644 --- a/worlds/clique/__init__.py +++ b/worlds/clique/__init__.py @@ -37,7 +37,6 @@ class CliqueWorld(World): """The greatest game of all time.""" game = "Clique" - data_version = 3 web = CliqueWebWorld() option_definitions = clique_options location_name_to_id = location_table diff --git a/worlds/cv64/__init__.py b/worlds/cv64/__init__.py index 2f483cd4d919..0d384acc8f3d 100644 --- a/worlds/cv64/__init__.py +++ b/worlds/cv64/__init__.py @@ -64,7 +64,6 @@ class CV64World(World): options: CV64Options settings: typing.ClassVar[CV64Settings] topology_present = True - data_version = 1 item_name_to_id = get_item_names_to_ids() location_name_to_id = get_location_names_to_ids() diff --git a/worlds/cv64/client.py b/worlds/cv64/client.py index ff9c79f578be..bea8ce38825d 100644 --- a/worlds/cv64/client.py +++ b/worlds/cv64/client.py @@ -146,7 +146,7 @@ async def game_watcher(self, ctx: "BizHawkClientContext") -> None: text_color = bytearray([0xA2, 0x0B]) else: text_color = bytearray([0xA2, 0x02]) - received_text, num_lines = cv64_text_wrap(f"{ctx.item_names[next_item.item]}\n" + received_text, num_lines = cv64_text_wrap(f"{ctx.item_names.lookup_in_slot(next_item.item)}\n" f"from {ctx.player_names[next_item.player]}", 96) await bizhawk.guarded_write(ctx.bizhawk_ctx, [(0x389BE1, [next_item.item & 0xFF], "RDRAM"), diff --git a/worlds/dark_souls_3/__init__.py b/worlds/dark_souls_3/__init__.py index c4b2232b32dc..020010981160 100644 --- a/worlds/dark_souls_3/__init__.py +++ b/worlds/dark_souls_3/__init__.py @@ -49,7 +49,6 @@ class DarkSouls3World(World): option_definitions = dark_souls_options topology_present: bool = True web = DarkSouls3Web() - data_version = 8 base_id = 100000 enabled_location_categories: Set[DS3LocationCategory] required_client_version = (0, 4, 2) diff --git a/worlds/dkc3/Client.py b/worlds/dkc3/Client.py index efa199e1d0c9..8e4a1bf2a423 100644 --- a/worlds/dkc3/Client.py +++ b/worlds/dkc3/Client.py @@ -86,7 +86,7 @@ async def game_watcher(self, ctx): for new_check_id in new_checks: ctx.locations_checked.add(new_check_id) - location = ctx.location_names[new_check_id] + location = ctx.location_names.lookup_in_slot(new_check_id) snes_logger.info( f'New Check: {location} ({len(ctx.locations_checked)}/{len(ctx.missing_locations) + len(ctx.checked_locations)})') await ctx.send_msgs([{"cmd": 'LocationChecks', "locations": [new_check_id]}]) @@ -99,9 +99,9 @@ async def game_watcher(self, ctx): item = ctx.items_received[recv_index] recv_index += 1 logging.info('Received %s from %s (%s) (%d/%d in list)' % ( - color(ctx.item_names[item.item], 'red', 'bold'), + color(ctx.item_names.lookup_in_slot(item.item), 'red', 'bold'), color(ctx.player_names[item.player], 'yellow'), - ctx.location_names[item.location], recv_index, len(ctx.items_received))) + ctx.location_names.lookup_in_slot(item.location, item.player), recv_index, len(ctx.items_received))) snes_buffered_write(ctx, DKC3_RECV_PROGRESS_ADDR, bytes([recv_index])) if item.item in item_rom_data: diff --git a/worlds/dkc3/__init__.py b/worlds/dkc3/__init__.py index f298114905fd..de6fb4a44a03 100644 --- a/worlds/dkc3/__init__.py +++ b/worlds/dkc3/__init__.py @@ -61,7 +61,6 @@ class DKC3World(World): options: DKC3Options topology_present = False - data_version = 2 #hint_blacklist = {LocationName.rocket_rush_flag} item_name_to_id = {name: data.code for name, data in item_table.items()} diff --git a/worlds/dlcquest/__init__.py b/worlds/dlcquest/__init__.py index ca2862113fd4..a9dfcc5044b1 100644 --- a/worlds/dlcquest/__init__.py +++ b/worlds/dlcquest/__init__.py @@ -43,8 +43,6 @@ class DLCqworld(World): item_name_to_id = {name: data.code for name, data in item_table.items()} location_name_to_id = location_table - data_version = 1 - options_dataclass = DLCQuestOptions options: DLCQuestOptions diff --git a/worlds/doom_1993/__init__.py b/worlds/doom_1993/__init__.py index ace33f994c33..b6138ae07103 100644 --- a/worlds/doom_1993/__init__.py +++ b/worlds/doom_1993/__init__.py @@ -42,7 +42,6 @@ class DOOM1993World(World): options: DOOM1993Options game = "DOOM 1993" web = DOOM1993Web() - data_version = 3 required_client_version = (0, 3, 9) item_name_to_id = {data["name"]: item_id for item_id, data in Items.item_table.items()} diff --git a/worlds/doom_ii/__init__.py b/worlds/doom_ii/__init__.py index daad94553517..38840f552a13 100644 --- a/worlds/doom_ii/__init__.py +++ b/worlds/doom_ii/__init__.py @@ -43,7 +43,6 @@ class DOOM2World(World): options: DOOM2Options game = "DOOM II" web = DOOM2Web() - data_version = 3 required_client_version = (0, 3, 9) item_name_to_id = {data["name"]: item_id for item_id, data in Items.item_table.items()} diff --git a/worlds/factorio/Client.py b/worlds/factorio/Client.py index d245e1bb7af6..258a5445328c 100644 --- a/worlds/factorio/Client.py +++ b/worlds/factorio/Client.py @@ -247,7 +247,7 @@ async def game_watcher(ctx: FactorioContext): if ctx.locations_checked != research_data: bridge_logger.debug( f"New researches done: " - f"{[ctx.location_names[rid] for rid in research_data - ctx.locations_checked]}") + f"{[ctx.location_names.lookup_in_slot(rid) for rid in research_data - ctx.locations_checked]}") ctx.locations_checked = research_data await ctx.send_msgs([{"cmd": 'LocationChecks', "locations": tuple(research_data)}]) death_link_tick = data.get("death_link_tick", 0) @@ -360,7 +360,7 @@ async def factorio_server_watcher(ctx: FactorioContext): transfer_item: NetworkItem = ctx.items_received[ctx.send_index] item_id = transfer_item.item player_name = ctx.player_names[transfer_item.player] - item_name = ctx.item_names[item_id] + item_name = ctx.item_names.lookup_in_slot(item_id) factorio_server_logger.info(f"Sending {item_name} to Nauvis from {player_name}.") commands[ctx.send_index] = f"/ap-get-technology {item_name}\t{ctx.send_index}\t{player_name}" ctx.send_index += 1 diff --git a/worlds/factorio/__init__.py b/worlds/factorio/__init__.py index 3b7475738489..1ea2f6e4c98c 100644 --- a/worlds/factorio/__init__.py +++ b/worlds/factorio/__init__.py @@ -95,7 +95,6 @@ class Factorio(World): item_name_groups = { "Progressive": set(progressive_tech_table.keys()), } - data_version = 8 required_client_version = (0, 4, 2) ordered_science_packs: typing.List[str] = MaxSciencePack.get_ordered_science_packs() diff --git a/worlds/ff1/__init__.py b/worlds/ff1/__init__.py index ce5519b13a1a..3a5047506850 100644 --- a/worlds/ff1/__init__.py +++ b/worlds/ff1/__init__.py @@ -40,7 +40,6 @@ class FF1World(World): settings_key = "ffr_options" game = "Final Fantasy" topology_present = False - data_version = 2 ff1_items = FF1Items() ff1_locations = FF1Locations() diff --git a/worlds/ffmq/__init__.py b/worlds/ffmq/__init__.py index b995cc427c9b..ac3e91370933 100644 --- a/worlds/ffmq/__init__.py +++ b/worlds/ffmq/__init__.py @@ -56,8 +56,6 @@ class FFMQWorld(World): create_regions = create_regions set_rules = set_rules stage_set_rules = stage_set_rules - - data_version = 1 web = FFMQWebWorld() # settings: FFMQSettings @@ -216,4 +214,3 @@ def extend_hint_information(self, hint_data): hint_data[self.player][location.address] += f"/{hint}" else: hint_data[self.player][location.address] = hint - diff --git a/worlds/generic/__init__.py b/worlds/generic/__init__.py index 6b2ffdfee180..b88295b43237 100644 --- a/worlds/generic/__init__.py +++ b/worlds/generic/__init__.py @@ -40,7 +40,6 @@ class GenericWorld(World): } hidden = True web = GenericWeb() - data_version = 1 def generate_early(self): self.multiworld.player_types[self.player] = SlotType.spectator # mark as spectator diff --git a/worlds/heretic/__init__.py b/worlds/heretic/__init__.py index c83cdb9477b2..fc5ffdd2de2b 100644 --- a/worlds/heretic/__init__.py +++ b/worlds/heretic/__init__.py @@ -41,7 +41,6 @@ class HereticWorld(World): options: HereticOptions game = "Heretic" web = HereticWeb() - data_version = 3 required_client_version = (0, 3, 9) item_name_to_id = {data["name"]: item_id for item_id, data in Items.item_table.items()} diff --git a/worlds/hk/__init__.py b/worlds/hk/__init__.py index 3530030fa695..fdaece8d34cd 100644 --- a/worlds/hk/__init__.py +++ b/worlds/hk/__init__.py @@ -154,7 +154,6 @@ class HKWorld(World): ranges: typing.Dict[str, typing.Tuple[int, int]] charm_costs: typing.List[int] cached_filler_items = {} - data_version = 2 def __init__(self, world, player): super(HKWorld, self).__init__(world, player) diff --git a/worlds/hylics2/__init__.py b/worlds/hylics2/__init__.py index be7ebf199127..18bcb0edc143 100644 --- a/worlds/hylics2/__init__.py +++ b/worlds/hylics2/__init__.py @@ -37,8 +37,6 @@ class Hylics2World(World): options_dataclass = Hylics2Options options: Hylics2Options - data_version = 3 - def set_rules(self): Rules.set_rules(self) diff --git a/worlds/kdl3/Client.py b/worlds/kdl3/Client.py index e33a680bc025..6faa8206c2df 100644 --- a/worlds/kdl3/Client.py +++ b/worlds/kdl3/Client.py @@ -330,9 +330,9 @@ async def game_watcher(self, ctx) -> None: item = ctx.items_received[recv_amount] recv_amount += 1 logging.info('Received %s from %s (%s) (%d/%d in list)' % ( - color(ctx.item_names[item.item], 'red', 'bold'), + color(ctx.item_names.lookup_in_slot(item.item), 'red', 'bold'), color(ctx.player_names[item.player], 'yellow'), - ctx.location_names[item.location], recv_amount, len(ctx.items_received))) + ctx.location_names.lookup_in_slot(item.location, item.player), recv_amount, len(ctx.items_received))) snes_buffered_write(ctx, KDL3_RECV_COUNT, pack("H", recv_amount)) item_idx = item.item & 0x00000F @@ -415,7 +415,7 @@ async def game_watcher(self, ctx) -> None: for new_check_id in new_checks: ctx.locations_checked.add(new_check_id) - location = ctx.location_names[new_check_id] + location = ctx.location_names.lookup_in_slot(new_check_id) snes_logger.info( f'New Check: {location} ({len(ctx.locations_checked)}/' f'{len(ctx.missing_locations) + len(ctx.checked_locations)})') diff --git a/worlds/ladx/__init__.py b/worlds/ladx/__init__.py index 6c7517f359dc..f7de0f41f9c0 100644 --- a/worlds/ladx/__init__.py +++ b/worlds/ladx/__init__.py @@ -78,11 +78,6 @@ class LinksAwakeningWorld(World): settings: typing.ClassVar[LinksAwakeningSettings] topology_present = True # show path to required location checks in spoiler - # data_version is used to signal that items, locations or their names - # changed. Set this to 0 during development so other games' clients do not - # cache any texts, then increase by 1 for each release that makes changes. - data_version = 1 - # ID of first item and location, could be hard-coded but code may be easier # to read with this as a propery. base_id = BASE_ID diff --git a/worlds/lingo/__init__.py b/worlds/lingo/__init__.py index fa24fdc3bc63..302e7e1d85b2 100644 --- a/worlds/lingo/__init__.py +++ b/worlds/lingo/__init__.py @@ -37,7 +37,6 @@ class LingoWorld(World): base_id = 444400 topology_present = True - data_version = 1 options_dataclass = LingoOptions options: LingoOptions diff --git a/worlds/lufia2ac/Client.py b/worlds/lufia2ac/Client.py index 3c05e6395d90..1e8437d20e84 100644 --- a/worlds/lufia2ac/Client.py +++ b/worlds/lufia2ac/Client.py @@ -147,9 +147,9 @@ async def game_watcher(self, ctx: SNIContext) -> None: snes_items_received += 1 snes_logger.info("Received %s from %s (%s) (%d/%d in list)" % ( - ctx.item_names[item.item], + ctx.item_names.lookup_in_slot(item.item), ctx.player_names[item.player], - ctx.location_names[item.location], + ctx.location_names.lookup_in_slot(item.location, item.player), snes_items_received, len(ctx.items_received))) snes_buffered_write(ctx, L2AC_RX_ADDR + 2 * (snes_items_received + 1), item_code.to_bytes(2, "little")) snes_buffered_write(ctx, L2AC_RX_ADDR, snes_items_received.to_bytes(2, "little")) diff --git a/worlds/lufia2ac/__init__.py b/worlds/lufia2ac/__init__.py index 561429c825f3..6433452cefea 100644 --- a/worlds/lufia2ac/__init__.py +++ b/worlds/lufia2ac/__init__.py @@ -65,7 +65,6 @@ class L2ACWorld(World): "Iris treasures": {name for name, data in l2ac_item_table.items() if data.type is ItemType.IRIS_TREASURE}, "Party members": {name for name, data in l2ac_item_table.items() if data.type is ItemType.PARTY_MEMBER}, } - data_version: ClassVar[int] = 2 required_client_version: Tuple[int, int, int] = (0, 4, 4) # L2ACWorld specific properties diff --git a/worlds/meritous/__init__.py b/worlds/meritous/__init__.py index 728d7af8616d..7a21b19ef247 100644 --- a/worlds/meritous/__init__.py +++ b/worlds/meritous/__init__.py @@ -44,8 +44,6 @@ class MeritousWorld(World): location_name_to_id = location_table item_name_groups = item_groups - data_version = 2 - # NOTE: Remember to change this before this game goes live required_client_version = (0, 2, 4) diff --git a/worlds/minecraft/__init__.py b/worlds/minecraft/__init__.py index 343b9bad19a9..75e043d0cbaf 100644 --- a/worlds/minecraft/__init__.py +++ b/worlds/minecraft/__init__.py @@ -92,8 +92,6 @@ class MinecraftWorld(World): item_name_to_id = Constants.item_name_to_id location_name_to_id = Constants.location_name_to_id - data_version = 7 - def _get_mc_data(self) -> Dict[str, Any]: exits = [connection[0] for connection in Constants.region_info["default_connections"]] return { diff --git a/worlds/mmbn3/__init__.py b/worlds/mmbn3/__init__.py index eac8a37bf06d..97725e728bae 100644 --- a/worlds/mmbn3/__init__.py +++ b/worlds/mmbn3/__init__.py @@ -57,8 +57,6 @@ class MMBN3World(World): settings: typing.ClassVar[MMBN3Settings] topology_present = False - data_version = 1 - item_name_to_id = {name: data.code for name, data in item_table.items()} location_name_to_id = {loc_data.name: loc_data.id for loc_data in all_locations} diff --git a/worlds/noita/__init__.py b/worlds/noita/__init__.py index 43078c5e4320..af2921768d6a 100644 --- a/worlds/noita/__init__.py +++ b/worlds/noita/__init__.py @@ -34,14 +34,13 @@ class NoitaWorld(World): item_name_groups = items.item_name_groups location_name_groups = locations.location_name_groups - data_version = 2 web = NoitaWeb() def generate_early(self) -> None: if not self.multiworld.get_player_name(self.player).isascii(): raise Exception("Noita yaml's slot name has invalid character(s).") - + # Returned items will be sent over to the client def fill_slot_data(self) -> Dict[str, Any]: return self.options.as_dict("death_link", "victory_condition", "path_option", "hidden_chests", diff --git a/worlds/oot/__init__.py b/worlds/oot/__init__.py index d9ee63850eaf..9346ac55103a 100644 --- a/worlds/oot/__init__.py +++ b/worlds/oot/__init__.py @@ -150,8 +150,6 @@ class OOTWorld(World): location_name_to_id = location_name_to_id web = OOTWeb() - data_version = 3 - required_client_version = (0, 4, 0) item_name_groups = { diff --git a/worlds/overcooked2/__init__.py b/worlds/overcooked2/__init__.py index be66fa3a8a1e..44227d4becaa 100644 --- a/worlds/overcooked2/__init__.py +++ b/worlds/overcooked2/__init__.py @@ -48,7 +48,6 @@ class Overcooked2World(World): web = Overcooked2Web() required_client_version = (0, 3, 8) topology_present: bool = False - data_version = 3 item_name_to_id = item_name_to_id item_id_to_name = item_id_to_name diff --git a/worlds/pokemon_emerald/__init__.py b/worlds/pokemon_emerald/__init__.py index 6225350a5e51..3e50f748d922 100644 --- a/worlds/pokemon_emerald/__init__.py +++ b/worlds/pokemon_emerald/__init__.py @@ -87,7 +87,6 @@ class PokemonEmeraldWorld(World): item_name_groups = ITEM_GROUPS location_name_groups = LOCATION_GROUPS - data_version = 2 required_client_version = (0, 4, 6) badge_shuffle_info: Optional[List[Tuple[PokemonEmeraldLocation, PokemonEmeraldItem]]] diff --git a/worlds/pokemon_rb/__init__.py b/worlds/pokemon_rb/__init__.py index 79028a68b187..003e0a32e97d 100644 --- a/worlds/pokemon_rb/__init__.py +++ b/worlds/pokemon_rb/__init__.py @@ -74,7 +74,6 @@ class PokemonRedBlueWorld(World): option_definitions = pokemon_rb_options settings: typing.ClassVar[PokemonSettings] - data_version = 9 required_client_version = (0, 4, 2) topology_present = True diff --git a/worlds/raft/__init__.py b/worlds/raft/__init__.py index 8e4eda09e10f..e96cd4471268 100644 --- a/worlds/raft/__init__.py +++ b/worlds/raft/__init__.py @@ -39,7 +39,6 @@ class RaftWorld(World): location_name_to_id = locations_lookup_name_to_id option_definitions = raft_options - data_version = 2 required_client_version = (0, 3, 4) def create_items(self): diff --git a/worlds/rogue_legacy/__init__.py b/worlds/rogue_legacy/__init__.py index c5a8d71b5d63..eb657699540f 100644 --- a/worlds/rogue_legacy/__init__.py +++ b/worlds/rogue_legacy/__init__.py @@ -35,7 +35,6 @@ class RLWorld(World): game = "Rogue Legacy" option_definitions = rl_options topology_present = True - data_version = 4 required_client_version = (0, 3, 5) web = RLWeb() diff --git a/worlds/ror2/__init__.py b/worlds/ror2/__init__.py index 5afdb797e7de..b6a1901a8db1 100644 --- a/worlds/ror2/__init__.py +++ b/worlds/ror2/__init__.py @@ -44,7 +44,6 @@ class RiskOfRainWorld(World): } location_name_to_id = item_pickups - data_version = 9 required_client_version = (0, 4, 5) web = RiskOfWeb() total_revivals: int diff --git a/worlds/sa2b/__init__.py b/worlds/sa2b/__init__.py index 6279aa943263..f7d1ca72b09f 100644 --- a/worlds/sa2b/__init__.py +++ b/worlds/sa2b/__init__.py @@ -58,7 +58,6 @@ class SA2BWorld(World): options_dataclass = SA2BOptions options: SA2BOptions topology_present = False - data_version = 7 item_name_groups = item_groups item_name_to_id = {name: data.code for name, data in item_table.items()} diff --git a/worlds/sc2/Client.py b/worlds/sc2/Client.py index 4e55509dda48..ac9ccfffcdbc 100644 --- a/worlds/sc2/Client.py +++ b/worlds/sc2/Client.py @@ -244,10 +244,10 @@ def print_faction_title(): self.formatted_print(f" [u]{faction.name}[/u] ") for item_id in categorized_items[faction]: - item_name = self.ctx.item_names[item_id] + item_name = self.ctx.item_names.lookup_in_slot(item_id) received_child_items = items_received_set.intersection(parent_to_child.get(item_id, [])) matching_children = [child for child in received_child_items - if item_matches_filter(self.ctx.item_names[child])] + if item_matches_filter(self.ctx.item_names.lookup_in_slot(child))] received_items_of_this_type = items_received.get(item_id, []) item_is_match = item_matches_filter(item_name) if item_is_match or len(matching_children) > 0: @@ -1165,7 +1165,7 @@ def request_unfinished_missions(ctx: SC2Context) -> None: objectives = set(ctx.locations_for_mission(mission)) if objectives: remaining_objectives = objectives.difference(ctx.checked_locations) - unfinished_locations[mission] = [ctx.location_names[location_id] for location_id in remaining_objectives] + unfinished_locations[mission] = [ctx.location_names.lookup_in_slot(location_id) for location_id in remaining_objectives] else: unfinished_locations[mission] = [] diff --git a/worlds/sc2/ClientGui.py b/worlds/sc2/ClientGui.py index 167583fd1ecb..f9dcfc18eb4a 100644 --- a/worlds/sc2/ClientGui.py +++ b/worlds/sc2/ClientGui.py @@ -269,7 +269,7 @@ def sort_unfinished_locations(self, mission_name: str) -> Tuple[Dict[LocationTyp for loc in self.ctx.locations_for_mission(mission_name): if loc in self.ctx.missing_locations: count += 1 - locations[lookup_location_id_to_type[loc]].append(self.ctx.location_names[loc]) + locations[lookup_location_id_to_type[loc]].append(self.ctx.location_names.lookup_in_slot(loc)) plando_locations = [] for plando_loc in self.ctx.plando_locations: diff --git a/worlds/shorthike/__init__.py b/worlds/shorthike/__init__.py index 3e0430f024ca..470b061c4bc0 100644 --- a/worlds/shorthike/__init__.py +++ b/worlds/shorthike/__init__.py @@ -28,7 +28,6 @@ class ShortHikeWorld(World): game = "A Short Hike" web = ShortHikeWeb() - data_version = 2 item_name_to_id = {item["name"]: item["id"] for item in item_table} location_name_to_id = {loc["name"]: loc["id"] for loc in location_table} diff --git a/worlds/sm/Client.py b/worlds/sm/Client.py index 7c97f743c552..6d6dd08ba5df 100644 --- a/worlds/sm/Client.py +++ b/worlds/sm/Client.py @@ -123,7 +123,7 @@ async def game_watcher(self, ctx): location_id = locations_start_id + item_index ctx.locations_checked.add(location_id) - location = ctx.location_names[location_id] + location = ctx.location_names.lookup_in_slot(location_id) snes_logger.info( f'New Check: {location} ({len(ctx.locations_checked)}/{len(ctx.missing_locations) + len(ctx.checked_locations)})') await ctx.send_msgs([{"cmd": 'LocationChecks', "locations": [location_id]}]) @@ -151,9 +151,8 @@ async def game_watcher(self, ctx): snes_buffered_write(ctx, SM_RECV_QUEUE_WCOUNT, bytes([item_out_ptr & 0xFF, (item_out_ptr >> 8) & 0xFF])) logging.info('Received %s from %s (%s) (%d/%d in list)' % ( - color(ctx.item_names[item.item], 'red', 'bold'), + color(ctx.item_names.lookup_in_slot(item.item), 'red', 'bold'), color(ctx.player_names[item.player], 'yellow'), - ctx.location_names[item.location], item_out_ptr, len(ctx.items_received))) + ctx.location_names.lookup_in_slot(item.location, item.player), item_out_ptr, len(ctx.items_received))) await snes_flush_writes(ctx) - diff --git a/worlds/sm/__init__.py b/worlds/sm/__init__.py index 7f12bf484c0f..826b1447793d 100644 --- a/worlds/sm/__init__.py +++ b/worlds/sm/__init__.py @@ -99,7 +99,6 @@ class SMWorld(World): game: str = "Super Metroid" topology_present = True - data_version = 3 option_definitions = sm_options settings: typing.ClassVar[SMSettings] diff --git a/worlds/sm64ex/__init__.py b/worlds/sm64ex/__init__.py index 0e944aa4ab4b..833ae56ca302 100644 --- a/worlds/sm64ex/__init__.py +++ b/worlds/sm64ex/__init__.py @@ -35,7 +35,6 @@ class SM64World(World): item_name_to_id = item_table location_name_to_id = location_table - data_version = 9 required_client_version = (0, 3, 5) area_connections: typing.Dict[int, int] diff --git a/worlds/smw/Client.py b/worlds/smw/Client.py index 33a74b3dc80f..85bb3fe1ee16 100644 --- a/worlds/smw/Client.py +++ b/worlds/smw/Client.py @@ -448,7 +448,7 @@ async def game_watcher(self, ctx): for new_check_id in new_checks: ctx.locations_checked.add(new_check_id) - location = ctx.location_names[new_check_id] + location = ctx.location_names.lookup_in_slot(new_check_id) snes_logger.info( f'New Check: {location} ({len(ctx.locations_checked)}/{len(ctx.missing_locations) + len(ctx.checked_locations)})') await ctx.send_msgs([{"cmd": 'LocationChecks', "locations": [new_check_id]}]) @@ -499,15 +499,16 @@ async def game_watcher(self, ctx): if recv_index < len(ctx.items_received): item = ctx.items_received[recv_index] recv_index += 1 + sending_game = ctx.slot_info[item.player].game logging.info('Received %s from %s (%s) (%d/%d in list)' % ( - color(ctx.item_names[item.item], 'red', 'bold'), + color(ctx.item_names.lookup_in_slot(item.item), 'red', 'bold'), color(ctx.player_names[item.player], 'yellow'), - ctx.location_names[item.location], recv_index, len(ctx.items_received))) + ctx.location_names.lookup_in_slot(item.location, item.player), recv_index, len(ctx.items_received))) if self.should_show_message(ctx, item): if item.item != 0xBC0012 and item.item not in trap_rom_data: # Don't send messages for Boss Tokens - item_name = ctx.item_names[item.item] + item_name = ctx.item_names.lookup_in_slot(item.item) player_name = ctx.player_names[item.player] receive_message = generate_received_text(item_name, player_name) @@ -515,7 +516,7 @@ async def game_watcher(self, ctx): snes_buffered_write(ctx, SMW_RECV_PROGRESS_ADDR, bytes([recv_index&0xFF, (recv_index>>8)&0xFF])) if item.item in trap_rom_data: - item_name = ctx.item_names[item.item] + item_name = ctx.item_names.lookup_in_slot(item.item) player_name = ctx.player_names[item.player] receive_message = generate_received_text(item_name, player_name) @@ -596,7 +597,7 @@ async def game_watcher(self, ctx): for loc_id in ctx.checked_locations: if loc_id not in ctx.locations_checked: ctx.locations_checked.add(loc_id) - loc_name = ctx.location_names[loc_id] + loc_name = ctx.location_names.lookup_in_slot(loc_id) if loc_name not in location_id_to_level_id: continue diff --git a/worlds/smz3/Client.py b/worlds/smz3/Client.py index 0a248aa5d3f2..3c90ead0064c 100644 --- a/worlds/smz3/Client.py +++ b/worlds/smz3/Client.py @@ -109,7 +109,7 @@ async def game_watcher(self, ctx): location_id = locations_start_id + convertLocSMZ3IDToAPID(item_index) ctx.locations_checked.add(location_id) - location = ctx.location_names[location_id] + location = ctx.location_names.lookup_in_slot(location_id) snes_logger.info(f'New Check: {location} ({len(ctx.locations_checked)}/{len(ctx.missing_locations) + len(ctx.checked_locations)})') await ctx.send_msgs([{"cmd": 'LocationChecks', "locations": [location_id]}]) @@ -132,8 +132,7 @@ async def game_watcher(self, ctx): item_out_ptr += 1 snes_buffered_write(ctx, SMZ3_RECV_PROGRESS_ADDR + recv_progress_addr_table_offset, bytes([item_out_ptr & 0xFF, (item_out_ptr >> 8) & 0xFF])) logging.info('Received %s from %s (%s) (%d/%d in list)' % ( - color(ctx.item_names[item.item], 'red', 'bold'), color(ctx.player_names[item.player], 'yellow'), - ctx.location_names[item.location], item_out_ptr, len(ctx.items_received))) + color(ctx.item_names.lookup_in_slot(item.item), 'red', 'bold'), color(ctx.player_names[item.player], 'yellow'), + ctx.location_names.lookup_in_slot(item.location, item.player), item_out_ptr, len(ctx.items_received))) await snes_flush_writes(ctx) - diff --git a/worlds/smz3/__init__.py b/worlds/smz3/__init__.py index b030e3fa50d2..6056a171d370 100644 --- a/worlds/smz3/__init__.py +++ b/worlds/smz3/__init__.py @@ -68,7 +68,6 @@ class SMZ3World(World): """ game: str = "SMZ3" topology_present = False - data_version = 3 option_definitions = smz3_options item_names: Set[str] = frozenset(TotalSMZ3Item.lookup_name_to_id) location_names: Set[str] diff --git a/worlds/soe/__init__.py b/worlds/soe/__init__.py index 061322650e68..3baed165d821 100644 --- a/worlds/soe/__init__.py +++ b/worlds/soe/__init__.py @@ -176,7 +176,6 @@ class SoEWorld(World): options: SoEOptions settings: typing.ClassVar[SoESettings] topology_present = False - data_version = 5 web = SoEWebWorld() required_client_version = (0, 4, 4) diff --git a/worlds/spire/__init__.py b/worlds/spire/__init__.py index d8a9322ab415..5b0e1e17f23d 100644 --- a/worlds/spire/__init__.py +++ b/worlds/spire/__init__.py @@ -30,7 +30,6 @@ class SpireWorld(World): option_definitions = spire_options game = "Slay the Spire" topology_present = False - data_version = 2 web = SpireWeb() required_client_version = (0, 3, 7) diff --git a/worlds/stardew_valley/__init__.py b/worlds/stardew_valley/__init__.py index dafb1c64730f..61c866631690 100644 --- a/worlds/stardew_valley/__init__.py +++ b/worlds/stardew_valley/__init__.py @@ -73,7 +73,6 @@ class StardewValleyWorld(World): [location.name for location in locations] for group, locations in locations_by_tag.items() } - data_version = 3 required_client_version = (0, 4, 0) options_dataclass = StardewValleyOptions diff --git a/worlds/subnautica/__init__.py b/worlds/subnautica/__init__.py index 08df70d78bbd..856117469e55 100644 --- a/worlds/subnautica/__init__.py +++ b/worlds/subnautica/__init__.py @@ -44,7 +44,6 @@ class SubnauticaWorld(World): location_name_to_id = all_locations options_dataclass = options.SubnauticaOptions options: options.SubnauticaOptions - data_version = 10 required_client_version = (0, 4, 1) creatures_to_scan: List[str] diff --git a/worlds/terraria/__init__.py b/worlds/terraria/__init__.py index ac6b25e51632..abc10a7bb37c 100644 --- a/worlds/terraria/__init__.py +++ b/worlds/terraria/__init__.py @@ -52,11 +52,6 @@ class TerrariaWorld(World): options_dataclass = TerrariaOptions options: TerrariaOptions - # data_version is used to signal that items, locations or their names - # changed. Set this to 0 during development so other games' clients do not - # cache any texts, then increase by 1 for each release that makes changes. - data_version = 2 - item_name_to_id = item_name_to_id location_name_to_id = location_name_to_id diff --git a/worlds/timespinner/__init__.py b/worlds/timespinner/__init__.py index ff7b3515e605..cab6fb648b95 100644 --- a/worlds/timespinner/__init__.py +++ b/worlds/timespinner/__init__.py @@ -39,7 +39,6 @@ class TimespinnerWorld(World): option_definitions = timespinner_options game = "Timespinner" topology_present = True - data_version = 12 web = TimespinnerWebWorld() required_client_version = (0, 4, 2) @@ -228,7 +227,7 @@ def assign_starter_items(self, excluded_items: Set[str]) -> None: non_local_items: Set[str] = self.multiworld.non_local_items[self.player].value local_items: Set[str] = self.multiworld.local_items[self.player].value - local_starter_melee_weapons = tuple(item for item in starter_melee_weapons if + local_starter_melee_weapons = tuple(item for item in starter_melee_weapons if item in local_items or not item in non_local_items) if not local_starter_melee_weapons: if 'Plasma Orb' in non_local_items: diff --git a/worlds/tloz/__init__.py b/worlds/tloz/__init__.py index 7565dc0147ce..a1f9081418e4 100644 --- a/worlds/tloz/__init__.py +++ b/worlds/tloz/__init__.py @@ -68,7 +68,6 @@ class TLoZWorld(World): settings: typing.ClassVar[TLoZSettings] game = "The Legend of Zelda" topology_present = False - data_version = 1 base_id = 7000 web = TLoZWeb() diff --git a/worlds/undertale/__init__.py b/worlds/undertale/__init__.py index 0694456a6b12..b87d3ac01e8f 100644 --- a/worlds/undertale/__init__.py +++ b/worlds/undertale/__init__.py @@ -52,8 +52,6 @@ class UndertaleWorld(World): item_name_to_id = {name: data.code for name, data in item_table.items()} location_name_to_id = {name: data.id for name, data in advancement_table.items()} - data_version = 7 - def _get_undertale_data(self): return { "world_seed": self.multiworld.per_slot_randoms[self.player].getrandbits(32), diff --git a/worlds/v6/__init__.py b/worlds/v6/__init__.py index 30a76f82cce6..3d3ee8cf58fd 100644 --- a/worlds/v6/__init__.py +++ b/worlds/v6/__init__.py @@ -34,8 +34,6 @@ class V6World(World): item_name_to_id = item_table location_name_to_id = location_table - data_version = 1 - area_connections: typing.Dict[int, int] area_cost_map: typing.Dict[int,int] diff --git a/worlds/yoshisisland/Client.py b/worlds/yoshisisland/Client.py index 1aff36c553c7..2a710b046a70 100644 --- a/worlds/yoshisisland/Client.py +++ b/worlds/yoshisisland/Client.py @@ -116,7 +116,7 @@ async def game_watcher(self, ctx: "SNIContext") -> None: for new_check_id in new_checks: ctx.locations_checked.add(new_check_id) - location = ctx.location_names[new_check_id] + location = ctx.location_names.lookup_in_slot(new_check_id) total_locations = len(ctx.missing_locations) + len(ctx.checked_locations) snes_logger.info(f"New Check: {location} ({len(ctx.locations_checked)}/{total_locations})") await ctx.send_msgs([{"cmd": "LocationChecks", "locations": [new_check_id]}]) @@ -127,9 +127,9 @@ async def game_watcher(self, ctx: "SNIContext") -> None: item = ctx.items_received[recv_index] recv_index += 1 logging.info("Received %s from %s (%s) (%d/%d in list)" % ( - color(ctx.item_names[item.item], "red", "bold"), + color(ctx.item_names.lookup_in_slot(item.item), "red", "bold"), color(ctx.player_names[item.player], "yellow"), - ctx.location_names[item.location], recv_index, len(ctx.items_received))) + ctx.location_names.lookup_in_slot(item.location, item.player), recv_index, len(ctx.items_received))) snes_buffered_write(ctx, ITEMQUEUE_HIGH, pack("H", recv_index)) if item.item in item_values: diff --git a/worlds/zillion/__init__.py b/worlds/zillion/__init__.py index cce120d7e3f4..205cc9ad6ba1 100644 --- a/worlds/zillion/__init__.py +++ b/worlds/zillion/__init__.py @@ -86,11 +86,6 @@ class ZillionWorld(World): item_name_to_id = _item_name_to_id location_name_to_id = _loc_name_to_id - # increment this every time something in your world's names/id mappings changes. - # While this is set to 0 in *any* AutoWorld, the entire DataPackage is considered in testing mode and will be - # retrieved by clients on every connection. - data_version = 1 - logger: logging.Logger class LogStreamInterface: From 2a5de8567e020c0d579bfffd68543c2b0cdc6ec0 Mon Sep 17 00:00:00 2001 From: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> Date: Sat, 1 Jun 2024 07:07:43 -0400 Subject: [PATCH 14/27] Docs: Making option description more readable and accurate (#3426) --- Options.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/Options.py b/Options.py index 7f480cbaae8d..995473243959 100644 --- a/Options.py +++ b/Options.py @@ -910,8 +910,10 @@ class Accessibility(Choice): class ProgressionBalancing(NamedRange): - """A system that can move progression earlier, to try and prevent the player from getting stuck and bored early. - A lower setting means more getting stuck. A higher setting means less getting stuck.""" + """ + A system that can move progression earlier, to try and prevent the player from getting stuck and bored early. + A lower setting means more getting stuck. A higher setting means less getting stuck. + """ default = 50 range_start = 0 range_end = 99 @@ -984,7 +986,7 @@ class LocalItems(ItemSet): class NonLocalItems(ItemSet): """Forces these items to be outside their native world.""" - display_name = "Not Local Items" + display_name = "Non-local Items" class StartInventory(ItemDict): From f2587d5d2773870a0323a92f895867959a997109 Mon Sep 17 00:00:00 2001 From: Louis M Date: Sat, 1 Jun 2024 07:09:34 -0400 Subject: [PATCH 15/27] Aquatia: Locations name changed due to typo's, grammar, or inconsistencies (#3421) --- worlds/aquaria/Items.py | 68 +-- worlds/aquaria/Locations.py | 364 +++++++-------- worlds/aquaria/Options.py | 2 +- worlds/aquaria/Regions.py | 438 +++++++++--------- worlds/aquaria/__init__.py | 22 +- worlds/aquaria/docs/en_Aquaria.md | 8 +- worlds/aquaria/test/__init__.py | 294 ++++++------ worlds/aquaria/test/test_beast_form_access.py | 38 +- worlds/aquaria/test/test_bind_song_access.py | 26 +- .../test/test_bind_song_option_access.py | 36 +- .../aquaria/test/test_confined_home_water.py | 4 +- worlds/aquaria/test/test_dual_song_access.py | 8 +- .../aquaria/test/test_energy_form_access.py | 68 +-- worlds/aquaria/test/test_fish_form_access.py | 20 +- worlds/aquaria/test/test_li_song_access.py | 42 +- worlds/aquaria/test/test_light_access.py | 56 +-- .../aquaria/test/test_nature_form_access.py | 56 +-- ...st_no_progression_hard_hidden_locations.py | 40 +- .../test_progression_hard_hidden_locations.py | 40 +- .../aquaria/test/test_spirit_form_access.py | 30 +- worlds/aquaria/test/test_sun_form_access.py | 6 +- .../test_unconfine_home_water_via_both.py | 4 +- ...st_unconfine_home_water_via_energy_door.py | 4 +- ...st_unconfine_home_water_via_transturtle.py | 2 +- 24 files changed, 838 insertions(+), 838 deletions(-) diff --git a/worlds/aquaria/Items.py b/worlds/aquaria/Items.py index 5494c87e8cf4..34557d95d00d 100644 --- a/worlds/aquaria/Items.py +++ b/worlds/aquaria/Items.py @@ -77,41 +77,41 @@ def __init__(self, id: int, count: int, type: ItemType, group: ItemGroup): item_table = { # name: ID, Nb, Item Type, Item Group "Anemone": ItemData(698000, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_anemone - "Arnassi statue": ItemData(698001, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_arnassi_statue - "Big seed": ItemData(698002, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_big_seed - "Glowing seed": ItemData(698003, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_bio_seed - "Black pearl": ItemData(698004, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_blackpearl - "Baby blaster": ItemData(698005, 1, ItemType.NORMAL, ItemGroup.UTILITY), # collectible_blaster - "Crab armor": ItemData(698006, 1, ItemType.NORMAL, ItemGroup.UTILITY), # collectible_crab_costume - "Baby dumbo": ItemData(698007, 1, ItemType.PROGRESSION, ItemGroup.UTILITY), # collectible_dumbo + "Arnassi Statue": ItemData(698001, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_arnassi_statue + "Big Seed": ItemData(698002, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_big_seed + "Glowing Seed": ItemData(698003, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_bio_seed + "Black Pearl": ItemData(698004, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_blackpearl + "Baby Blaster": ItemData(698005, 1, ItemType.NORMAL, ItemGroup.UTILITY), # collectible_blaster + "Crab Armor": ItemData(698006, 1, ItemType.NORMAL, ItemGroup.UTILITY), # collectible_crab_costume + "Baby Dumbo": ItemData(698007, 1, ItemType.PROGRESSION, ItemGroup.UTILITY), # collectible_dumbo "Tooth": ItemData(698008, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_energy_boss - "Energy statue": ItemData(698009, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_energy_statue - "Krotite armor": ItemData(698010, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_energy_temple - "Golden starfish": ItemData(698011, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_gold_star - "Golden gear": ItemData(698012, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_golden_gear - "Jelly beacon": ItemData(698013, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_jelly_beacon - "Jelly costume": ItemData(698014, 1, ItemType.NORMAL, ItemGroup.UTILITY), # collectible_jelly_costume - "Jelly plant": ItemData(698015, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_jelly_plant - "Mithalas doll": ItemData(698016, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_mithala_doll - "Mithalan dress": ItemData(698017, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_mithalan_costume - "Mithalas banner": ItemData(698018, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_mithalas_banner - "Mithalas pot": ItemData(698019, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_mithalas_pot - "Mutant costume": ItemData(698020, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_mutant_costume - "Baby nautilus": ItemData(698021, 1, ItemType.NORMAL, ItemGroup.UTILITY), # collectible_nautilus - "Baby piranha": ItemData(698022, 1, ItemType.NORMAL, ItemGroup.UTILITY), # collectible_piranha + "Energy Statue": ItemData(698009, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_energy_statue + "Krotite Armor": ItemData(698010, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_energy_temple + "Golden Starfish": ItemData(698011, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_gold_star + "Golden Gear": ItemData(698012, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_golden_gear + "Jelly Beacon": ItemData(698013, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_jelly_beacon + "Jelly Costume": ItemData(698014, 1, ItemType.NORMAL, ItemGroup.UTILITY), # collectible_jelly_costume + "Jelly Plant": ItemData(698015, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_jelly_plant + "Mithalas Doll": ItemData(698016, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_mithala_doll + "Mithalan Dress": ItemData(698017, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_mithalan_costume + "Mithalas Banner": ItemData(698018, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_mithalas_banner + "Mithalas Pot": ItemData(698019, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_mithalas_pot + "Mutant Costume": ItemData(698020, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_mutant_costume + "Baby Nautilus": ItemData(698021, 1, ItemType.NORMAL, ItemGroup.UTILITY), # collectible_nautilus + "Baby Piranha": ItemData(698022, 1, ItemType.NORMAL, ItemGroup.UTILITY), # collectible_piranha "Arnassi Armor": ItemData(698023, 1, ItemType.NORMAL, ItemGroup.UTILITY), # collectible_seahorse_costume - "Seed bag": ItemData(698024, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_seed_bag + "Seed Bag": ItemData(698024, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_seed_bag "King's Skull": ItemData(698025, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_skull - "Song plant spore": ItemData(698026, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_spore_seed - "Stone head": ItemData(698027, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_stone_head - "Sun key": ItemData(698028, 1, ItemType.NORMAL, ItemGroup.COLLECTIBLE), # collectible_sun_key - "Girl costume": ItemData(698029, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_teen_costume - "Odd container": ItemData(698030, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_treasure_chest + "Song Plant Spore": ItemData(698026, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_spore_seed + "Stone Head": ItemData(698027, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_stone_head + "Sun Key": ItemData(698028, 1, ItemType.NORMAL, ItemGroup.COLLECTIBLE), # collectible_sun_key + "Girl Costume": ItemData(698029, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_teen_costume + "Odd Container": ItemData(698030, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_treasure_chest "Trident": ItemData(698031, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_trident_head - "Turtle egg": ItemData(698032, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_turtle_egg - "Jelly egg": ItemData(698033, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_upsidedown_seed - "Urchin costume": ItemData(698034, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_urchin_costume - "Baby walker": ItemData(698035, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_walker + "Turtle Egg": ItemData(698032, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_turtle_egg + "Jelly Egg": ItemData(698033, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_upsidedown_seed + "Urchin Costume": ItemData(698034, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_urchin_costume + "Baby Walker": ItemData(698035, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_walker "Vedha's Cure-All-All": ItemData(698036, 1, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_Vedha'sCure-All "Zuuna's perogi": ItemData(698037, 1, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_Zuuna'sperogi "Arcane poultice": ItemData(698038, 7, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_arcanepoultice @@ -206,9 +206,9 @@ def __init__(self, id: int, count: int, type: ItemType, group: ItemGroup): "Transturtle Open Water top right": ItemData(698127, 1, ItemType.PROGRESSION, ItemGroup.TURTLE), # transport_openwater03 "Transturtle Forest bottom left": ItemData(698128, 1, ItemType.PROGRESSION, ItemGroup.TURTLE), # transport_forest04 - "Transturtle Home water": ItemData(698129, 1, ItemType.NORMAL, ItemGroup.TURTLE), # transport_mainarea + "Transturtle Home Water": ItemData(698129, 1, ItemType.NORMAL, ItemGroup.TURTLE), # transport_mainarea "Transturtle Abyss right": ItemData(698130, 1, ItemType.PROGRESSION, ItemGroup.TURTLE), # transport_abyss03 "Transturtle Final Boss": ItemData(698131, 1, ItemType.PROGRESSION, ItemGroup.TURTLE), # transport_finalboss - "Transturtle Simon says": ItemData(698132, 1, ItemType.PROGRESSION, ItemGroup.TURTLE), # transport_forest05 - "Transturtle Arnassi ruins": ItemData(698133, 1, ItemType.PROGRESSION, ItemGroup.TURTLE), # transport_seahorse + "Transturtle Simon Says": ItemData(698132, 1, ItemType.PROGRESSION, ItemGroup.TURTLE), # transport_forest05 + "Transturtle Arnassi Ruins": ItemData(698133, 1, ItemType.PROGRESSION, ItemGroup.TURTLE), # transport_seahorse } diff --git a/worlds/aquaria/Locations.py b/worlds/aquaria/Locations.py index e4f6f104ccf9..7360efde065e 100644 --- a/worlds/aquaria/Locations.py +++ b/worlds/aquaria/Locations.py @@ -29,213 +29,213 @@ def __init__(self, player: int, name="", code=None, parent=None) -> None: class AquariaLocations: locations_verse_cave_r = { - "Verse cave, bulb in the skeleton room": 698107, - "Verse cave, bulb in the path left of the skeleton room": 698108, - "Verse cave right area, Big Seed": 698175, + "Verse Cave, bulb in the skeleton room": 698107, + "Verse Cave, bulb in the path left of the skeleton room": 698108, + "Verse Cave right area, Big Seed": 698175, } locations_verse_cave_l = { - "Verse cave, the Naija hint about here shield ability": 698200, - "Verse cave left area, bulb in the center part": 698021, - "Verse cave left area, bulb in the right part": 698022, - "Verse cave left area, bulb under the rock at the end of the path": 698023, + "Verse Cave, the Naija hint about the shield ability": 698200, + "Verse Cave left area, bulb in the center part": 698021, + "Verse Cave left area, bulb in the right part": 698022, + "Verse Cave left area, bulb under the rock at the end of the path": 698023, } locations_home_water = { - "Home water, bulb below the grouper fish": 698058, - "Home water, bulb in the path below Nautilus Prime": 698059, - "Home water, bulb in the little room above the grouper fish": 698060, - "Home water, bulb in the end of the left path from the verse cave": 698061, - "Home water, bulb in the top left path": 698062, - "Home water, bulb in the bottom left room": 698063, - "Home water, bulb close to the Naija's home": 698064, - "Home water, bulb under the rock in the left path from the verse cave": 698065, + "Home Water, bulb below the grouper fish": 698058, + "Home Water, bulb in the path below Nautilus Prime": 698059, + "Home Water, bulb in the little room above the grouper fish": 698060, + "Home Water, bulb in the end of the left path from the Verse Cave": 698061, + "Home Water, bulb in the top left path": 698062, + "Home Water, bulb in the bottom left room": 698063, + "Home Water, bulb close to Naija's Home": 698064, + "Home Water, bulb under the rock in the left path from the Verse Cave": 698065, } locations_home_water_nautilus = { - "Home water, Nautilus Egg": 698194, + "Home Water, Nautilus Egg": 698194, } locations_home_water_transturtle = { - "Home water, Transturtle": 698213, + "Home Water, Transturtle": 698213, } locations_naija_home = { - "Naija's home, bulb after the energy door": 698119, - "Naija's home, bulb under the rock at the right of the main path": 698120, + "Naija's Home, bulb after the energy door": 698119, + "Naija's Home, bulb under the rock at the right of the main path": 698120, } locations_song_cave = { - "Song cave, Erulian spirit": 698206, - "Song cave, bulb in the top left part": 698071, - "Song cave, bulb in the big anemone room": 698072, - "Song cave, bulb in the path to the singing statues": 698073, - "Song cave, bulb under the rock in the path to the singing statues": 698074, - "Song cave, bulb under the rock close to the song door": 698075, - "Song cave, Verse egg": 698160, - "Song cave, Jelly beacon": 698178, - "Song cave, Anemone seed": 698162, + "Song Cave, Erulian spirit": 698206, + "Song Cave, bulb in the top left part": 698071, + "Song Cave, bulb in the big anemone room": 698072, + "Song Cave, bulb in the path to the singing statues": 698073, + "Song Cave, bulb under the rock in the path to the singing statues": 698074, + "Song Cave, bulb under the rock close to the song door": 698075, + "Song Cave, Verse Egg": 698160, + "Song Cave, Jelly Beacon": 698178, + "Song Cave, Anemone Seed": 698162, } locations_energy_temple_1 = { - "Energy temple first area, beating the energy statue": 698205, - "Energy temple first area, bulb in the bottom room blocked by a rock": 698027, + "Energy Temple first area, beating the Energy Statue": 698205, + "Energy Temple first area, bulb in the bottom room blocked by a rock": 698027, } locations_energy_temple_idol = { - "Energy temple first area, Energy Idol": 698170, + "Energy Temple first area, Energy Idol": 698170, } locations_energy_temple_2 = { - "Energy temple second area, bulb under the rock": 698028, + "Energy Temple second area, bulb under the rock": 698028, } locations_energy_temple_altar = { - "Energy temple bottom entrance, Krotite armor": 698163, + "Energy Temple bottom entrance, Krotite Armor": 698163, } locations_energy_temple_3 = { - "Energy temple third area, bulb in the bottom path": 698029, + "Energy Temple third area, bulb in the bottom path": 698029, } locations_energy_temple_boss = { - "Energy temple boss area, Fallen god tooth": 698169, + "Energy Temple boss area, Fallen God Tooth": 698169, } locations_energy_temple_blaster_room = { - "Energy temple blaster room, Blaster egg": 698195, + "Energy Temple blaster room, Blaster Egg": 698195, } locations_openwater_tl = { - "Open water top left area, bulb under the rock in the right path": 698001, - "Open water top left area, bulb under the rock in the left path": 698002, - "Open water top left area, bulb to the right of the save cristal": 698003, + "Open Water top left area, bulb under the rock in the right path": 698001, + "Open Water top left area, bulb under the rock in the left path": 698002, + "Open Water top left area, bulb to the right of the save crystal": 698003, } locations_openwater_tr = { - "Open water top right area, bulb in the small path before Mithalas": 698004, - "Open water top right area, bulb in the path from the left entrance": 698005, - "Open water top right area, bulb in the clearing close to the bottom exit": 698006, - "Open water top right area, bulb in the big clearing close to the save cristal": 698007, - "Open water top right area, bulb in the big clearing to the top exit": 698008, - "Open water top right area, first urn in the Mithalas exit": 698148, - "Open water top right area, second urn in the Mithalas exit": 698149, - "Open water top right area, third urn in the Mithalas exit": 698150, + "Open Water top right area, bulb in the small path before Mithalas": 698004, + "Open Water top right area, bulb in the path from the left entrance": 698005, + "Open Water top right area, bulb in the clearing close to the bottom exit": 698006, + "Open Water top right area, bulb in the big clearing close to the save crystal": 698007, + "Open Water top right area, bulb in the big clearing to the top exit": 698008, + "Open Water top right area, first urn in the Mithalas exit": 698148, + "Open Water top right area, second urn in the Mithalas exit": 698149, + "Open Water top right area, third urn in the Mithalas exit": 698150, } locations_openwater_tr_turtle = { - "Open water top right area, bulb in the turtle room": 698009, - "Open water top right area, Transturtle": 698211, + "Open Water top right area, bulb in the turtle room": 698009, + "Open Water top right area, Transturtle": 698211, } locations_openwater_bl = { - "Open water bottom left area, bulb behind the chomper fish": 698011, - "Open water bottom left area, bulb inside the lowest fish pass": 698010, + "Open Water bottom left area, bulb behind the chomper fish": 698011, + "Open Water bottom left area, bulb inside the lowest fish pass": 698010, } locations_skeleton_path = { - "Open water skeleton path, bulb close to the right exit": 698012, - "Open water skeleton path, bulb behind the chomper fish": 698013, + "Open Water skeleton path, bulb close to the right exit": 698012, + "Open Water skeleton path, bulb behind the chomper fish": 698013, } locations_skeleton_path_sc = { - "Open water skeleton path, King skull": 698177, + "Open Water skeleton path, King Skull": 698177, } locations_arnassi = { "Arnassi Ruins, bulb in the right part": 698014, "Arnassi Ruins, bulb in the left part": 698015, "Arnassi Ruins, bulb in the center part": 698016, - "Arnassi ruins, Song plant spore on the top of the ruins": 698179, - "Arnassi ruins, Arnassi Armor": 698191, + "Arnassi Ruins, Song Plant Spore": 698179, + "Arnassi Ruins, Arnassi Armor": 698191, } locations_arnassi_path = { - "Arnassi Ruins, Arnassi statue": 698164, + "Arnassi Ruins, Arnassi Statue": 698164, "Arnassi Ruins, Transturtle": 698217, } locations_arnassi_crab_boss = { - "Arnassi ruins, Crab armor": 698187, + "Arnassi Ruins, Crab Armor": 698187, } locations_simon = { - "Kelp forest, beating Simon says": 698156, - "Simon says area, Transturtle": 698216, + "Simon Says area, beating Simon Says": 698156, + "Simon Says area, Transturtle": 698216, } locations_mithalas_city = { - "Mithalas city, first bulb in the left city part": 698030, - "Mithalas city, second bulb in the left city part": 698035, - "Mithalas city, bulb in the right part": 698031, - "Mithalas city, bulb at the top of the city": 698033, - "Mithalas city, first bulb in a broken home": 698034, - "Mithalas city, second bulb in a broken home": 698041, - "Mithalas city, bulb in the bottom left part": 698037, - "Mithalas city, first bulb in one of the homes": 698038, - "Mithalas city, second bulb in one of the homes": 698039, - "Mithalas city, first urn in one of the homes": 698123, - "Mithalas city, second urn in one of the homes": 698124, - "Mithalas city, first urn in the city reserve": 698125, - "Mithalas city, second urn in the city reserve": 698126, - "Mithalas city, third urn in the city reserve": 698127, + "Mithalas City, first bulb in the left city part": 698030, + "Mithalas City, second bulb in the left city part": 698035, + "Mithalas City, bulb in the right part": 698031, + "Mithalas City, bulb at the top of the city": 698033, + "Mithalas City, first bulb in a broken home": 698034, + "Mithalas City, second bulb in a broken home": 698041, + "Mithalas City, bulb in the bottom left part": 698037, + "Mithalas City, first bulb in one of the homes": 698038, + "Mithalas City, second bulb in one of the homes": 698039, + "Mithalas City, first urn in one of the homes": 698123, + "Mithalas City, second urn in one of the homes": 698124, + "Mithalas City, first urn in the city reserve": 698125, + "Mithalas City, second urn in the city reserve": 698126, + "Mithalas City, third urn in the city reserve": 698127, } locations_mithalas_city_top_path = { - "Mithalas city, first bulb at the end of the top path": 698032, - "Mithalas city, second bulb at the end of the top path": 698040, - "Mithalas city, bulb in the top path": 698036, - "Mithalas city, Mithalas pot": 698174, - "Mithalas city, urn in the cathedral flower tube entrance": 698128, + "Mithalas City, first bulb at the end of the top path": 698032, + "Mithalas City, second bulb at the end of the top path": 698040, + "Mithalas City, bulb in the top path": 698036, + "Mithalas City, Mithalas Pot": 698174, + "Mithalas City, urn in the Cathedral flower tube entrance": 698128, } locations_mithalas_city_fishpass = { - "Mithalas city, Doll": 698173, - "Mithalas city, urn inside a home fish pass": 698129, + "Mithalas City, Doll": 698173, + "Mithalas City, urn inside a home fish pass": 698129, } locations_cathedral_l = { - "Mithalas city castle, bulb in the flesh hole": 698042, - "Mithalas city castle, Blue banner": 698165, - "Mithalas city castle, urn in the bedroom": 698130, - "Mithalas city castle, first urn of the single lamp path": 698131, - "Mithalas city castle, second urn of the single lamp path": 698132, - "Mithalas city castle, urn in the bottom room": 698133, - "Mithalas city castle, first urn on the entrance path": 698134, - "Mithalas city castle, second urn on the entrance path": 698135, + "Mithalas City Castle, bulb in the flesh hole": 698042, + "Mithalas City Castle, Blue banner": 698165, + "Mithalas City Castle, urn in the bedroom": 698130, + "Mithalas City Castle, first urn of the single lamp path": 698131, + "Mithalas City Castle, second urn of the single lamp path": 698132, + "Mithalas City Castle, urn in the bottom room": 698133, + "Mithalas City Castle, first urn on the entrance path": 698134, + "Mithalas City Castle, second urn on the entrance path": 698135, } locations_cathedral_l_tube = { - "Mithalas castle, beating the priests": 698208, + "Mithalas City Castle, beating the Priests": 698208, } locations_cathedral_l_sc = { - "Mithalas city castle, Trident head": 698183, + "Mithalas City Castle, Trident Head": 698183, } locations_cathedral_r = { - "Mithalas cathedral, first urn in the top right room": 698136, - "Mithalas cathedral, second urn in the top right room": 698137, - "Mithalas cathedral, third urn in the top right room": 698138, - "Mithalas cathedral, urn in the flesh room with fleas": 698139, - "Mithalas cathedral, first urn in the bottom right path": 698140, - "Mithalas cathedral, second urn in the bottom right path": 698141, - "Mithalas cathedral, urn behind the flesh vein": 698142, - "Mithalas cathedral, urn in the top left eyes boss room": 698143, - "Mithalas cathedral, first urn in the path behind the flesh vein": 698144, - "Mithalas cathedral, second urn in the path behind the flesh vein": 698145, - "Mithalas cathedral, third urn in the path behind the flesh vein": 698146, - "Mithalas cathedral, one of the urns in the top right room": 698147, - "Mithalas cathedral, Mithalan Dress": 698189, - "Mithalas cathedral right area, urn below the left entrance": 698198, + "Mithalas Cathedral, first urn in the top right room": 698136, + "Mithalas Cathedral, second urn in the top right room": 698137, + "Mithalas Cathedral, third urn in the top right room": 698138, + "Mithalas Cathedral, urn in the flesh room with fleas": 698139, + "Mithalas Cathedral, first urn in the bottom right path": 698140, + "Mithalas Cathedral, second urn in the bottom right path": 698141, + "Mithalas Cathedral, urn behind the flesh vein": 698142, + "Mithalas Cathedral, urn in the top left eyes boss room": 698143, + "Mithalas Cathedral, first urn in the path behind the flesh vein": 698144, + "Mithalas Cathedral, second urn in the path behind the flesh vein": 698145, + "Mithalas Cathedral, third urn in the path behind the flesh vein": 698146, + "Mithalas Cathedral, fourth urn in the top right room": 698147, + "Mithalas Cathedral, Mithalan Dress": 698189, + "Mithalas Cathedral right area, urn below the left entrance": 698198, } locations_cathedral_underground = { - "Cathedral underground, bulb in the center part": 698113, - "Cathedral underground, first bulb in the top left part": 698114, - "Cathedral underground, second bulb in the top left part": 698115, - "Cathedral underground, third bulb in the top left part": 698116, - "Cathedral underground, bulb close to the save cristal": 698117, - "Cathedral underground, bulb in the bottom right path": 698118, + "Cathedral Underground, bulb in the center part": 698113, + "Cathedral Underground, first bulb in the top left part": 698114, + "Cathedral Underground, second bulb in the top left part": 698115, + "Cathedral Underground, third bulb in the top left part": 698116, + "Cathedral Underground, bulb close to the save crystal": 698117, + "Cathedral Underground, bulb in the bottom right path": 698118, } locations_cathedral_boss = { @@ -250,8 +250,8 @@ class AquariaLocations: } locations_forest_tl_fp = { - "Kelp Forest top left area, bulb close to the Verse egg": 698047, - "Kelp forest top left area, Verse egg": 698158, + "Kelp Forest top left area, bulb close to the Verse Egg": 698047, + "Kelp Forest top left area, Verse Egg": 698158, } locations_forest_tr = { @@ -260,7 +260,7 @@ class AquariaLocations: "Kelp Forest top right area, bulb in the left path's big room": 698051, "Kelp Forest top right area, bulb in the left path's small room": 698052, "Kelp Forest top right area, bulb at the top of the center clearing": 698053, - "Kelp forest top right area, Black pearl": 698167, + "Kelp Forest top right area, Black Pearl": 698167, } locations_forest_tr_fp = { @@ -269,16 +269,16 @@ class AquariaLocations: locations_forest_bl = { "Kelp Forest bottom left area, bulb close to the spirit crystals": 698054, - "Kelp forest bottom left area, Walker baby": 698186, + "Kelp Forest bottom left area, Walker baby": 698186, "Kelp Forest bottom left area, Transturtle": 698212, } locations_forest_br = { - "Kelp forest bottom right area, Odd Container": 698168, + "Kelp Forest bottom right area, Odd Container": 698168, } locations_forest_boss = { - "Kelp forest boss area, beating Drunian God": 698204, + "Kelp Forest boss area, beating Drunian God": 698204, } locations_forest_boss_entrance = { @@ -286,7 +286,7 @@ class AquariaLocations: } locations_forest_fish_cave = { - "Kelp Forest bottom left area, Fish cave puzzle": 698207, + "Kelp Forest bottom left area, Fish Cave puzzle": 698207, } locations_forest_sprite_cave = { @@ -295,7 +295,7 @@ class AquariaLocations: locations_forest_sprite_cave_tube = { "Kelp Forest sprite cave, bulb in the second room": 698057, - "Kelp Forest Sprite Cave, Seed bag": 698176, + "Kelp Forest sprite cave, Seed Bag": 698176, } locations_mermog_cave = { @@ -307,14 +307,14 @@ class AquariaLocations: } locations_veil_tl = { - "The veil top left area, In the Li cave": 698199, - "The veil top left area, bulb under the rock in the top right path": 698078, - "The veil top left area, bulb hidden behind the blocking rock": 698076, - "The veil top left area, Transturtle": 698209, + "The Veil top left area, In Li's cave": 698199, + "The Veil top left area, bulb under the rock in the top right path": 698078, + "The Veil top left area, bulb hidden behind the blocking rock": 698076, + "The Veil top left area, Transturtle": 698209, } locations_veil_tl_fp = { - "The veil top left area, bulb inside the fish pass": 698077, + "The Veil top left area, bulb inside the fish pass": 698077, } locations_turtle_cave = { @@ -322,56 +322,56 @@ class AquariaLocations: } locations_turtle_cave_bubble = { - "Turtle cave, bulb in bubble cliff": 698000, - "Turtle cave, Urchin costume": 698193, + "Turtle cave, bulb in Bubble Cliff": 698000, + "Turtle cave, Urchin Costume": 698193, } locations_veil_tr_r = { - "The veil top right area, bulb in the middle of the wall jump cliff": 698079, - "The veil top right area, golden starfish at the bottom right of the bottom path": 698180, + "The Veil top right area, bulb in the middle of the wall jump cliff": 698079, + "The Veil top right area, Golden Starfish": 698180, } locations_veil_tr_l = { - "The veil top right area, bulb in the top of the water fall": 698080, - "The veil top right area, Transturtle": 698210, + "The Veil top right area, bulb in the top of the waterfall": 698080, + "The Veil top right area, Transturtle": 698210, } locations_veil_bl = { - "The veil bottom area, bulb in the left path": 698082, + "The Veil bottom area, bulb in the left path": 698082, } locations_veil_b_sc = { - "The veil bottom area, bulb in the spirit path": 698081, + "The Veil bottom area, bulb in the spirit path": 698081, } locations_veil_bl_fp = { - "The veil bottom area, Verse egg": 698157, + "The Veil bottom area, Verse Egg": 698157, } locations_veil_br = { - "The veil bottom area, Stone Head": 698181, + "The Veil bottom area, Stone Head": 698181, } locations_octo_cave_t = { - "Octopus cave, Dumbo Egg": 698196, + "Octopus Cave, Dumbo Egg": 698196, } locations_octo_cave_b = { - "Octopus cave, bulb in the path below the octopus cave path": 698122, + "Octopus Cave, bulb in the path below the Octopus Cave path": 698122, } locations_sun_temple_l = { - "Sun temple, bulb in the top left part": 698094, - "Sun temple, bulb in the top right part": 698095, - "Sun temple, bulb at the top of the high dark room": 698096, - "Sun temple, Golden Gear": 698171, + "Sun Temple, bulb in the top left part": 698094, + "Sun Temple, bulb in the top right part": 698095, + "Sun Temple, bulb at the top of the high dark room": 698096, + "Sun Temple, Golden Gear": 698171, } locations_sun_temple_r = { - "Sun temple, first bulb of the temple": 698091, - "Sun temple, bulb on the left part": 698092, - "Sun temple, bulb in the hidden room of the right part": 698093, - "Sun temple, Sun key": 698182, + "Sun Temple, first bulb of the temple": 698091, + "Sun Temple, bulb on the left part": 698092, + "Sun Temple, bulb in the hidden room of the right part": 698093, + "Sun Temple, Sun Key": 698182, } locations_sun_temple_boss_path = { @@ -382,13 +382,13 @@ class AquariaLocations: } locations_sun_temple_boss = { - "Sun temple boss area, beating Sun God": 698203, + "Sun Temple boss area, beating Sun God": 698203, } locations_abyss_l = { "Abyss left area, bulb in hidden path room": 698024, "Abyss left area, bulb in the right part": 698025, - "Abyss left area, Glowing seed": 698166, + "Abyss left area, Glowing Seed": 698166, "Abyss left area, Glowing Plant": 698172, } @@ -405,87 +405,87 @@ class AquariaLocations: } locations_ice_cave = { - "Ice cave, bulb in the room to the right": 698083, - "Ice cave, First bulbs in the top exit room": 698084, - "Ice cave, Second bulbs in the top exit room": 698085, - "Ice cave, third bulbs in the top exit room": 698086, - "Ice cave, bulb in the left room": 698087, + "Ice Cave, bulb in the room to the right": 698083, + "Ice Cave, first bulb in the top exit room": 698084, + "Ice Cave, second bulb in the top exit room": 698085, + "Ice Cave, third bulb in the top exit room": 698086, + "Ice Cave, bulb in the left room": 698087, } locations_bubble_cave = { - "Bubble cave, bulb in the left cave wall": 698089, - "Bubble cave, bulb in the right cave wall (behind the ice cristal)": 698090, + "Bubble Cave, bulb in the left cave wall": 698089, + "Bubble Cave, bulb in the right cave wall (behind the ice crystal)": 698090, } locations_bubble_cave_boss = { - "Bubble cave, Verse egg": 698161, + "Bubble Cave, Verse Egg": 698161, } locations_king_jellyfish_cave = { - "King Jellyfish cave, bulb in the right path from King Jelly": 698088, - "King Jellyfish cave, Jellyfish Costume": 698188, + "King Jellyfish Cave, bulb in the right path from King Jelly": 698088, + "King Jellyfish Cave, Jellyfish Costume": 698188, } locations_whale = { - "The whale, Verse egg": 698159, + "The Whale, Verse Egg": 698159, } locations_sunken_city_r = { - "Sunken city right area, crate close to the save cristal": 698154, - "Sunken city right area, crate in the left bottom room": 698155, + "Sunken City right area, crate close to the save crystal": 698154, + "Sunken City right area, crate in the left bottom room": 698155, } locations_sunken_city_l = { - "Sunken city left area, crate in the little pipe room": 698151, - "Sunken city left area, crate close to the save cristal": 698152, - "Sunken city left area, crate before the bedroom": 698153, + "Sunken City left area, crate in the little pipe room": 698151, + "Sunken City left area, crate close to the save crystal": 698152, + "Sunken City left area, crate before the bedroom": 698153, } locations_sunken_city_l_bedroom = { - "Sunken city left area, Girl Costume": 698192, + "Sunken City left area, Girl Costume": 698192, } locations_sunken_city_boss = { - "Sunken city, bulb on the top of the boss area (boiler room)": 698043, + "Sunken City, bulb on top of the boss area": 698043, } locations_body_c = { - "The body center area, breaking li cage": 698201, - "The body main area, bulb on the main path blocking tube": 698097, + "The Body center area, breaking Li's cage": 698201, + "The Body main area, bulb on the main path blocking tube": 698097, } locations_body_l = { - "The body left area, first bulb in the top face room": 698066, - "The body left area, second bulb in the top face room": 698069, - "The body left area, bulb below the water stream": 698067, - "The body left area, bulb in the top path to the top face room": 698068, - "The body left area, bulb in the bottom face room": 698070, + "The Body left area, first bulb in the top face room": 698066, + "The Body left area, second bulb in the top face room": 698069, + "The Body left area, bulb below the water stream": 698067, + "The Body left area, bulb in the top path to the top face room": 698068, + "The Body left area, bulb in the bottom face room": 698070, } locations_body_rt = { - "The body right area, bulb in the top face room": 698100, + "The Body right area, bulb in the top face room": 698100, } locations_body_rb = { - "The body right area, bulb in the top path to the bottom face room": 698098, - "The body right area, bulb in the bottom face room": 698099, + "The Body right area, bulb in the top path to the bottom face room": 698098, + "The Body right area, bulb in the bottom face room": 698099, } locations_body_b = { - "The body bottom area, bulb in the Jelly Zap room": 698101, - "The body bottom area, bulb in the nautilus room": 698102, - "The body bottom area, Mutant Costume": 698190, + "The Body bottom area, bulb in the Jelly Zap room": 698101, + "The Body bottom area, bulb in the nautilus room": 698102, + "The Body bottom area, Mutant Costume": 698190, } locations_final_boss_tube = { - "Final boss area, first bulb in the turtle room": 698103, - "Final boss area, second bulbs in the turtle room": 698104, - "Final boss area, third bulbs in the turtle room": 698105, - "Final boss area, Transturtle": 698215, + "Final Boss area, first bulb in the turtle room": 698103, + "Final Boss area, second bulb in the turtle room": 698104, + "Final Boss area, third bulb in the turtle room": 698105, + "Final Boss area, Transturtle": 698215, } locations_final_boss = { - "Final boss area, bulb in the boss third form room": 698106, + "Final Boss area, bulb in the boss third form room": 698106, } diff --git a/worlds/aquaria/Options.py b/worlds/aquaria/Options.py index 9a49e915b9cd..4c795d350898 100644 --- a/worlds/aquaria/Options.py +++ b/worlds/aquaria/Options.py @@ -113,7 +113,7 @@ class BindSongNeededToGetUnderRockBulb(Toggle): class UnconfineHomeWater(Choice): """ - Open the way out of the Home water area so that Naija can go to open water and beyond without the bind song. + Open the way out of the Home Water area so that Naija can go to open water and beyond without the bind song. """ display_name = "Unconfine Home Water Area" option_off = 0 diff --git a/worlds/aquaria/Regions.py b/worlds/aquaria/Regions.py index 5956e0ca842a..f2f85749f3fb 100755 --- a/worlds/aquaria/Regions.py +++ b/worlds/aquaria/Regions.py @@ -36,8 +36,8 @@ def _has_li(state:CollectionState, player: int) -> bool: def _has_damaging_item(state:CollectionState, player: int) -> bool: """`player` in `state` has the shield song item""" - return state.has_any({"Energy form", "Nature form", "Beast form", "Li and Li song", "Baby nautilus", - "Baby piranha", "Baby blaster"}, player) + return state.has_any({"Energy form", "Nature form", "Beast form", "Li and Li song", "Baby Nautilus", + "Baby Piranha", "Baby Blaster"}, player) def _has_shield_song(state:CollectionState, player: int) -> bool: @@ -72,7 +72,7 @@ def _has_sun_form(state:CollectionState, player: int) -> bool: def _has_light(state:CollectionState, player: int) -> bool: """`player` in `state` has the light item""" - return state.has("Baby dumbo", player) or _has_sun_form(state, player) + return state.has("Baby Dumbo", player) or _has_sun_form(state, player) def _has_dual_form(state:CollectionState, player: int) -> bool: @@ -237,26 +237,26 @@ def __create_home_water_area(self) -> None: AquariaLocations.locations_home_water_nautilus) self.home_water_transturtle = self.__add_region("Home Water, turtle room", AquariaLocations.locations_home_water_transturtle) - self.naija_home = self.__add_region("Naija's home", AquariaLocations.locations_naija_home) - self.song_cave = self.__add_region("Song cave", AquariaLocations.locations_song_cave) + self.naija_home = self.__add_region("Naija's Home", AquariaLocations.locations_naija_home) + self.song_cave = self.__add_region("Song Cave", AquariaLocations.locations_song_cave) def __create_energy_temple(self) -> None: """ Create the `energy_temple_*` regions """ - self.energy_temple_1 = self.__add_region("Energy temple first area", + self.energy_temple_1 = self.__add_region("Energy Temple first area", AquariaLocations.locations_energy_temple_1) - self.energy_temple_2 = self.__add_region("Energy temple second area", + self.energy_temple_2 = self.__add_region("Energy Temple second area", AquariaLocations.locations_energy_temple_2) - self.energy_temple_3 = self.__add_region("Energy temple third area", + self.energy_temple_3 = self.__add_region("Energy Temple third area", AquariaLocations.locations_energy_temple_3) - self.energy_temple_altar = self.__add_region("Energy temple bottom entrance", + self.energy_temple_altar = self.__add_region("Energy Temple bottom entrance", AquariaLocations.locations_energy_temple_altar) - self.energy_temple_boss = self.__add_region("Energy temple fallen God room", + self.energy_temple_boss = self.__add_region("Energy Temple fallen God room", AquariaLocations.locations_energy_temple_boss) - self.energy_temple_idol = self.__add_region("Energy temple Idol room", + self.energy_temple_idol = self.__add_region("Energy Temple Idol room", AquariaLocations.locations_energy_temple_idol) - self.energy_temple_blaster_room = self.__add_region("Energy temple blaster room", + self.energy_temple_blaster_room = self.__add_region("Energy Temple blaster room", AquariaLocations.locations_energy_temple_blaster_room) def __create_openwater(self) -> None: @@ -264,18 +264,18 @@ def __create_openwater(self) -> None: Create the `openwater_*`, `skeleton_path`, `arnassi*` and `simon` regions """ - self.openwater_tl = self.__add_region("Open water top left area", + self.openwater_tl = self.__add_region("Open Water top left area", AquariaLocations.locations_openwater_tl) - self.openwater_tr = self.__add_region("Open water top right area", + self.openwater_tr = self.__add_region("Open Water top right area", AquariaLocations.locations_openwater_tr) - self.openwater_tr_turtle = self.__add_region("Open water top right area, turtle room", + self.openwater_tr_turtle = self.__add_region("Open Water top right area, turtle room", AquariaLocations.locations_openwater_tr_turtle) - self.openwater_bl = self.__add_region("Open water bottom left area", + self.openwater_bl = self.__add_region("Open Water bottom left area", AquariaLocations.locations_openwater_bl) - self.openwater_br = self.__add_region("Open water bottom right area", None) - self.skeleton_path = self.__add_region("Open water skeleton path", + self.openwater_br = self.__add_region("Open Water bottom right area", None) + self.skeleton_path = self.__add_region("Open Water skeleton path", AquariaLocations.locations_skeleton_path) - self.skeleton_path_sc = self.__add_region("Open water skeleton path spirit cristal", + self.skeleton_path_sc = self.__add_region("Open Water skeleton path spirit crystal", AquariaLocations.locations_skeleton_path_sc) self.arnassi = self.__add_region("Arnassi Ruins", AquariaLocations.locations_arnassi) self.arnassi_path = self.__add_region("Arnassi Ruins, back entrance path", @@ -287,20 +287,20 @@ def __create_mithalas(self) -> None: """ Create the `mithalas_city*` and `cathedral_*` regions """ - self.mithalas_city = self.__add_region("Mithalas city", + self.mithalas_city = self.__add_region("Mithalas City", AquariaLocations.locations_mithalas_city) - self.mithalas_city_fishpass = self.__add_region("Mithalas city fish pass", + self.mithalas_city_fishpass = self.__add_region("Mithalas City fish pass", AquariaLocations.locations_mithalas_city_fishpass) - self.mithalas_city_top_path = self.__add_region("Mithalas city top path", + self.mithalas_city_top_path = self.__add_region("Mithalas City top path", AquariaLocations.locations_mithalas_city_top_path) self.cathedral_l = self.__add_region("Mithalas castle", AquariaLocations.locations_cathedral_l) self.cathedral_l_tube = self.__add_region("Mithalas castle, plant tube entrance", AquariaLocations.locations_cathedral_l_tube) - self.cathedral_l_sc = self.__add_region("Mithalas castle spirit cristal", + self.cathedral_l_sc = self.__add_region("Mithalas castle spirit crystal", AquariaLocations.locations_cathedral_l_sc) self.cathedral_r = self.__add_region("Mithalas Cathedral", AquariaLocations.locations_cathedral_r) - self.cathedral_underground = self.__add_region("Mithalas Cathedral underground area", + self.cathedral_underground = self.__add_region("Mithalas Cathedral Underground area", AquariaLocations.locations_cathedral_underground) self.cathedral_boss_r = self.__add_region("Mithalas Cathedral, Mithalan God room", AquariaLocations.locations_cathedral_boss) @@ -310,73 +310,73 @@ def __create_forest(self) -> None: """ Create the `forest_*` dans `mermog_cave` regions """ - self.forest_tl = self.__add_region("Kelp forest top left area", + self.forest_tl = self.__add_region("Kelp Forest top left area", AquariaLocations.locations_forest_tl) - self.forest_tl_fp = self.__add_region("Kelp forest top left area fish pass", + self.forest_tl_fp = self.__add_region("Kelp Forest top left area fish pass", AquariaLocations.locations_forest_tl_fp) - self.forest_tr = self.__add_region("Kelp forest top right area", + self.forest_tr = self.__add_region("Kelp Forest top right area", AquariaLocations.locations_forest_tr) - self.forest_tr_fp = self.__add_region("Kelp forest top right area fish pass", + self.forest_tr_fp = self.__add_region("Kelp Forest top right area fish pass", AquariaLocations.locations_forest_tr_fp) - self.forest_bl = self.__add_region("Kelp forest bottom left area", + self.forest_bl = self.__add_region("Kelp Forest bottom left area", AquariaLocations.locations_forest_bl) - self.forest_br = self.__add_region("Kelp forest bottom right area", + self.forest_br = self.__add_region("Kelp Forest bottom right area", AquariaLocations.locations_forest_br) - self.forest_sprite_cave = self.__add_region("Kelp forest spirit cave", + self.forest_sprite_cave = self.__add_region("Kelp Forest spirit cave", AquariaLocations.locations_forest_sprite_cave) - self.forest_sprite_cave_tube = self.__add_region("Kelp forest spirit cave after the plant tube", + self.forest_sprite_cave_tube = self.__add_region("Kelp Forest spirit cave after the plant tube", AquariaLocations.locations_forest_sprite_cave_tube) - self.forest_boss = self.__add_region("Kelp forest Drunian God room", + self.forest_boss = self.__add_region("Kelp Forest Drunian God room", AquariaLocations.locations_forest_boss) - self.forest_boss_entrance = self.__add_region("Kelp forest Drunian God room entrance", + self.forest_boss_entrance = self.__add_region("Kelp Forest Drunian God room entrance", AquariaLocations.locations_forest_boss_entrance) - self.mermog_cave = self.__add_region("Kelp forest Mermog cave", + self.mermog_cave = self.__add_region("Kelp Forest Mermog cave", AquariaLocations.locations_mermog_cave) - self.mermog_boss = self.__add_region("Kelp forest Mermog cave boss", + self.mermog_boss = self.__add_region("Kelp Forest Mermog cave boss", AquariaLocations.locations_mermog_boss) - self.forest_fish_cave = self.__add_region("Kelp forest fish cave", + self.forest_fish_cave = self.__add_region("Kelp Forest fish cave", AquariaLocations.locations_forest_fish_cave) - self.simon = self.__add_region("Kelp forest, Simon's room", AquariaLocations.locations_simon) + self.simon = self.__add_region("Kelp Forest, Simon's room", AquariaLocations.locations_simon) def __create_veil(self) -> None: """ Create the `veil_*`, `octo_cave` and `turtle_cave` regions """ - self.veil_tl = self.__add_region("The veil top left area", AquariaLocations.locations_veil_tl) - self.veil_tl_fp = self.__add_region("The veil top left area fish pass", + self.veil_tl = self.__add_region("The Veil top left area", AquariaLocations.locations_veil_tl) + self.veil_tl_fp = self.__add_region("The Veil top left area fish pass", AquariaLocations.locations_veil_tl_fp) - self.turtle_cave = self.__add_region("The veil top left area, turtle cave", + self.turtle_cave = self.__add_region("The Veil top left area, turtle cave", AquariaLocations.locations_turtle_cave) - self.turtle_cave_bubble = self.__add_region("The veil top left area, turtle cave bubble cliff", + self.turtle_cave_bubble = self.__add_region("The Veil top left area, turtle cave Bubble Cliff", AquariaLocations.locations_turtle_cave_bubble) - self.veil_tr_l = self.__add_region("The veil top right area, left of temple", + self.veil_tr_l = self.__add_region("The Veil top right area, left of temple", AquariaLocations.locations_veil_tr_l) - self.veil_tr_r = self.__add_region("The veil top right area, right of temple", + self.veil_tr_r = self.__add_region("The Veil top right area, right of temple", AquariaLocations.locations_veil_tr_r) - self.octo_cave_t = self.__add_region("Octopus cave top entrance", + self.octo_cave_t = self.__add_region("Octopus Cave top entrance", AquariaLocations.locations_octo_cave_t) - self.octo_cave_b = self.__add_region("Octopus cave bottom entrance", + self.octo_cave_b = self.__add_region("Octopus Cave bottom entrance", AquariaLocations.locations_octo_cave_b) - self.veil_bl = self.__add_region("The veil bottom left area", + self.veil_bl = self.__add_region("The Veil bottom left area", AquariaLocations.locations_veil_bl) - self.veil_b_sc = self.__add_region("The veil bottom spirit cristal area", + self.veil_b_sc = self.__add_region("The Veil bottom spirit crystal area", AquariaLocations.locations_veil_b_sc) - self.veil_bl_fp = self.__add_region("The veil bottom left area, in the sunken ship", + self.veil_bl_fp = self.__add_region("The Veil bottom left area, in the sunken ship", AquariaLocations.locations_veil_bl_fp) - self.veil_br = self.__add_region("The veil bottom right area", + self.veil_br = self.__add_region("The Veil bottom right area", AquariaLocations.locations_veil_br) def __create_sun_temple(self) -> None: """ Create the `sun_temple*` regions """ - self.sun_temple_l = self.__add_region("Sun temple left area", + self.sun_temple_l = self.__add_region("Sun Temple left area", AquariaLocations.locations_sun_temple_l) - self.sun_temple_r = self.__add_region("Sun temple right area", + self.sun_temple_r = self.__add_region("Sun Temple right area", AquariaLocations.locations_sun_temple_r) - self.sun_temple_boss_path = self.__add_region("Sun temple before boss area", + self.sun_temple_boss_path = self.__add_region("Sun Temple before boss area", AquariaLocations.locations_sun_temple_boss_path) - self.sun_temple_boss = self.__add_region("Sun temple boss area", + self.sun_temple_boss = self.__add_region("Sun Temple boss area", AquariaLocations.locations_sun_temple_boss) def __create_abyss(self) -> None: @@ -388,9 +388,9 @@ def __create_abyss(self) -> None: AquariaLocations.locations_abyss_l) self.abyss_lb = self.__add_region("Abyss left bottom area", AquariaLocations.locations_abyss_lb) self.abyss_r = self.__add_region("Abyss right area", AquariaLocations.locations_abyss_r) - self.ice_cave = self.__add_region("Ice cave", AquariaLocations.locations_ice_cave) - self.bubble_cave = self.__add_region("Bubble cave", AquariaLocations.locations_bubble_cave) - self.bubble_cave_boss = self.__add_region("Bubble cave boss area", AquariaLocations.locations_bubble_cave_boss) + self.ice_cave = self.__add_region("Ice Cave", AquariaLocations.locations_ice_cave) + self.bubble_cave = self.__add_region("Bubble Cave", AquariaLocations.locations_bubble_cave) + self.bubble_cave_boss = self.__add_region("Bubble Cave boss area", AquariaLocations.locations_bubble_cave_boss) self.king_jellyfish_cave = self.__add_region("Abyss left area, King jellyfish cave", AquariaLocations.locations_king_jellyfish_cave) self.whale = self.__add_region("Inside the whale", AquariaLocations.locations_whale) @@ -400,35 +400,35 @@ def __create_sunken_city(self) -> None: """ Create the `sunken_city_*` regions """ - self.sunken_city_l = self.__add_region("Sunken city left area", + self.sunken_city_l = self.__add_region("Sunken City left area", AquariaLocations.locations_sunken_city_l) - self.sunken_city_l_bedroom = self.__add_region("Sunken city left area, bedroom", + self.sunken_city_l_bedroom = self.__add_region("Sunken City left area, bedroom", AquariaLocations.locations_sunken_city_l_bedroom) - self.sunken_city_r = self.__add_region("Sunken city right area", + self.sunken_city_r = self.__add_region("Sunken City right area", AquariaLocations.locations_sunken_city_r) - self.sunken_city_boss = self.__add_region("Sunken city boss area", + self.sunken_city_boss = self.__add_region("Sunken City boss area", AquariaLocations.locations_sunken_city_boss) def __create_body(self) -> None: """ Create the `body_*` and `final_boss* regions """ - self.body_c = self.__add_region("The body center area", + self.body_c = self.__add_region("The Body center area", AquariaLocations.locations_body_c) - self.body_l = self.__add_region("The body left area", + self.body_l = self.__add_region("The Body left area", AquariaLocations.locations_body_l) - self.body_rt = self.__add_region("The body right area, top path", + self.body_rt = self.__add_region("The Body right area, top path", AquariaLocations.locations_body_rt) - self.body_rb = self.__add_region("The body right area, bottom path", + self.body_rb = self.__add_region("The Body right area, bottom path", AquariaLocations.locations_body_rb) - self.body_b = self.__add_region("The body bottom area", + self.body_b = self.__add_region("The Body bottom area", AquariaLocations.locations_body_b) - self.final_boss_loby = self.__add_region("The body, before final boss", None) - self.final_boss_tube = self.__add_region("The body, final boss area turtle room", + self.final_boss_loby = self.__add_region("The Body, before final boss", None) + self.final_boss_tube = self.__add_region("The Body, final boss area turtle room", AquariaLocations.locations_final_boss_tube) - self.final_boss = self.__add_region("The body, final boss", + self.final_boss = self.__add_region("The Body, final boss", AquariaLocations.locations_final_boss) - self.final_boss_end = self.__add_region("The body, final boss area", None) + self.final_boss_end = self.__add_region("The Body, final boss area", None) def __connect_one_way_regions(self, source_name: str, destination_name: str, source_region: Region, @@ -455,99 +455,99 @@ def __connect_home_water_regions(self) -> None: """ Connect entrances of the different regions around `home_water` """ - self.__connect_regions("Menu", "Verse cave right area", + self.__connect_regions("Menu", "Verse Cave right area", self.menu, self.verse_cave_r) - self.__connect_regions("Verse cave left area", "Verse cave right area", + self.__connect_regions("Verse Cave left area", "Verse Cave right area", self.verse_cave_l, self.verse_cave_r) - self.__connect_regions("Verse cave", "Home water", self.verse_cave_l, self.home_water) + self.__connect_regions("Verse Cave", "Home Water", self.verse_cave_l, self.home_water) self.__connect_regions("Home Water", "Haija's home", self.home_water, self.naija_home) - self.__connect_regions("Home Water", "Song cave", self.home_water, self.song_cave) - self.__connect_regions("Home Water", "Home water, nautilus nest", + self.__connect_regions("Home Water", "Song Cave", self.home_water, self.song_cave) + self.__connect_regions("Home Water", "Home Water, nautilus nest", self.home_water, self.home_water_nautilus, lambda state: _has_energy_form(state, self.player) and _has_bind_song(state, self.player)) - self.__connect_regions("Home Water", "Home water transturtle room", + self.__connect_regions("Home Water", "Home Water transturtle room", self.home_water, self.home_water_transturtle) - self.__connect_regions("Home Water", "Energy temple first area", + self.__connect_regions("Home Water", "Energy Temple first area", self.home_water, self.energy_temple_1, lambda state: _has_bind_song(state, self.player)) - self.__connect_regions("Home Water", "Energy temple_altar", + self.__connect_regions("Home Water", "Energy Temple_altar", self.home_water, self.energy_temple_altar, lambda state: _has_energy_form(state, self.player) and _has_bind_song(state, self.player)) - self.__connect_regions("Energy temple first area", "Energy temple second area", + self.__connect_regions("Energy Temple first area", "Energy Temple second area", self.energy_temple_1, self.energy_temple_2, lambda state: _has_energy_form(state, self.player)) - self.__connect_regions("Energy temple first area", "Energy temple idol room", + self.__connect_regions("Energy Temple first area", "Energy Temple idol room", self.energy_temple_1, self.energy_temple_idol, lambda state: _has_fish_form(state, self.player)) - self.__connect_regions("Energy temple idol room", "Energy temple boss area", + self.__connect_regions("Energy Temple idol room", "Energy Temple boss area", self.energy_temple_idol, self.energy_temple_boss, lambda state: _has_energy_form(state, self.player)) - self.__connect_one_way_regions("Energy temple first area", "Energy temple boss area", + self.__connect_one_way_regions("Energy Temple first area", "Energy Temple boss area", self.energy_temple_1, self.energy_temple_boss, lambda state: _has_beast_form(state, self.player) and _has_energy_form(state, self.player)) - self.__connect_one_way_regions("Energy temple boss area", "Energy temple first area", + self.__connect_one_way_regions("Energy Temple boss area", "Energy Temple first area", self.energy_temple_boss, self.energy_temple_1, lambda state: _has_energy_form(state, self.player)) - self.__connect_regions("Energy temple second area", "Energy temple third area", + self.__connect_regions("Energy Temple second area", "Energy Temple third area", self.energy_temple_2, self.energy_temple_3, lambda state: _has_bind_song(state, self.player) and _has_energy_form(state, self.player)) - self.__connect_regions("Energy temple boss area", "Energy temple blaster room", + self.__connect_regions("Energy Temple boss area", "Energy Temple blaster room", self.energy_temple_boss, self.energy_temple_blaster_room, lambda state: _has_nature_form(state, self.player) and _has_bind_song(state, self.player) and _has_energy_form(state, self.player)) - self.__connect_regions("Energy temple first area", "Energy temple blaster room", + self.__connect_regions("Energy Temple first area", "Energy Temple blaster room", self.energy_temple_1, self.energy_temple_blaster_room, lambda state: _has_nature_form(state, self.player) and _has_bind_song(state, self.player) and _has_energy_form(state, self.player) and _has_beast_form(state, self.player)) - self.__connect_regions("Home Water", "Open water top left area", + self.__connect_regions("Home Water", "Open Water top left area", self.home_water, self.openwater_tl) def __connect_open_water_regions(self) -> None: """ Connect entrances of the different regions around open water """ - self.__connect_regions("Open water top left area", "Open water top right area", + self.__connect_regions("Open Water top left area", "Open Water top right area", self.openwater_tl, self.openwater_tr) - self.__connect_regions("Open water top left area", "Open water bottom left area", + self.__connect_regions("Open Water top left area", "Open Water bottom left area", self.openwater_tl, self.openwater_bl) - self.__connect_regions("Open water top left area", "forest bottom right area", + self.__connect_regions("Open Water top left area", "forest bottom right area", self.openwater_tl, self.forest_br) - self.__connect_regions("Open water top right area", "Open water top right area, turtle room", + self.__connect_regions("Open Water top right area", "Open Water top right area, turtle room", self.openwater_tr, self.openwater_tr_turtle, lambda state: _has_beast_form(state, self.player)) - self.__connect_regions("Open water top right area", "Open water bottom right area", + self.__connect_regions("Open Water top right area", "Open Water bottom right area", self.openwater_tr, self.openwater_br) - self.__connect_regions("Open water top right area", "Mithalas city", + self.__connect_regions("Open Water top right area", "Mithalas City", self.openwater_tr, self.mithalas_city) - self.__connect_regions("Open water top right area", "Veil bottom left area", + self.__connect_regions("Open Water top right area", "Veil bottom left area", self.openwater_tr, self.veil_bl) - self.__connect_one_way_regions("Open water top right area", "Veil bottom right", + self.__connect_one_way_regions("Open Water top right area", "Veil bottom right", self.openwater_tr, self.veil_br, lambda state: _has_beast_form(state, self.player)) - self.__connect_one_way_regions("Veil bottom right", "Open water top right area", + self.__connect_one_way_regions("Veil bottom right", "Open Water top right area", self.veil_br, self.openwater_tr, lambda state: _has_beast_form(state, self.player)) - self.__connect_regions("Open water bottom left area", "Open water bottom right area", + self.__connect_regions("Open Water bottom left area", "Open Water bottom right area", self.openwater_bl, self.openwater_br) - self.__connect_regions("Open water bottom left area", "Skeleton path", + self.__connect_regions("Open Water bottom left area", "Skeleton path", self.openwater_bl, self.skeleton_path) - self.__connect_regions("Abyss left area", "Open water bottom left area", + self.__connect_regions("Abyss left area", "Open Water bottom left area", self.abyss_l, self.openwater_bl) self.__connect_regions("Skeleton path", "skeleton_path_sc", self.skeleton_path, self.skeleton_path_sc, lambda state: _has_spirit_form(state, self.player)) - self.__connect_regions("Abyss right area", "Open water bottom right area", + self.__connect_regions("Abyss right area", "Open Water bottom right area", self.abyss_r, self.openwater_br) - self.__connect_one_way_regions("Open water bottom right area", "Arnassi", + self.__connect_one_way_regions("Open Water bottom right area", "Arnassi", self.openwater_br, self.arnassi, lambda state: _has_beast_form(state, self.player)) - self.__connect_one_way_regions("Arnassi", "Open water bottom right area", + self.__connect_one_way_regions("Arnassi", "Open Water bottom right area", self.arnassi, self.openwater_br) self.__connect_regions("Arnassi", "Arnassi path", self.arnassi, self.arnassi_path) @@ -562,23 +562,23 @@ def __connect_mithalas_regions(self) -> None: """ Connect entrances of the different regions around Mithalas """ - self.__connect_one_way_regions("Mithalas city", "Mithalas city top path", + self.__connect_one_way_regions("Mithalas City", "Mithalas City top path", self.mithalas_city, self.mithalas_city_top_path, lambda state: _has_beast_form(state, self.player)) - self.__connect_one_way_regions("Mithalas city_top_path", "Mithalas city", + self.__connect_one_way_regions("Mithalas City_top_path", "Mithalas City", self.mithalas_city_top_path, self.mithalas_city) - self.__connect_regions("Mithalas city", "Mithalas city home with fishpass", + self.__connect_regions("Mithalas City", "Mithalas City home with fishpass", self.mithalas_city, self.mithalas_city_fishpass, lambda state: _has_fish_form(state, self.player)) - self.__connect_regions("Mithalas city", "Mithalas castle", + self.__connect_regions("Mithalas City", "Mithalas castle", self.mithalas_city, self.cathedral_l, lambda state: _has_fish_form(state, self.player)) - self.__connect_one_way_regions("Mithalas city top path", "Mithalas castle, flower tube", + self.__connect_one_way_regions("Mithalas City top path", "Mithalas castle, flower tube", self.mithalas_city_top_path, self.cathedral_l_tube, lambda state: _has_nature_form(state, self.player) and _has_energy_form(state, self.player)) - self.__connect_one_way_regions("Mithalas castle, flower tube area", "Mithalas city top path", + self.__connect_one_way_regions("Mithalas castle, flower tube area", "Mithalas City top path", self.cathedral_l_tube, self.mithalas_city_top_path, lambda state: _has_beast_form(state, self.player) and @@ -690,22 +690,22 @@ def __connect_veil_regions(self) -> None: self.veil_tl, self.veil_tr_r) self.__connect_regions("Veil top left area", "Turtle cave", self.veil_tl, self.turtle_cave) - self.__connect_regions("Turtle cave", "Turtle cave bubble cliff", + self.__connect_regions("Turtle cave", "Turtle cave Bubble Cliff", self.turtle_cave, self.turtle_cave_bubble, lambda state: _has_beast_form(state, self.player)) - self.__connect_regions("Veil right of sun temple", "Sun temple right area", + self.__connect_regions("Veil right of sun temple", "Sun Temple right area", self.veil_tr_r, self.sun_temple_r) - self.__connect_regions("Sun temple right area", "Sun temple left area", + self.__connect_regions("Sun Temple right area", "Sun Temple left area", self.sun_temple_r, self.sun_temple_l, lambda state: _has_bind_song(state, self.player)) - self.__connect_regions("Sun temple left area", "Veil left of sun temple", + self.__connect_regions("Sun Temple left area", "Veil left of sun temple", self.sun_temple_l, self.veil_tr_l) - self.__connect_regions("Sun temple left area", "Sun temple before boss area", + self.__connect_regions("Sun Temple left area", "Sun Temple before boss area", self.sun_temple_l, self.sun_temple_boss_path) - self.__connect_regions("Sun temple before boss area", "Sun temple boss area", + self.__connect_regions("Sun Temple before boss area", "Sun Temple boss area", self.sun_temple_boss_path, self.sun_temple_boss, lambda state: _has_energy_form(state, self.player)) - self.__connect_one_way_regions("Sun temple boss area", "Veil left of sun temple", + self.__connect_one_way_regions("Sun Temple boss area", "Veil left of sun temple", self.sun_temple_boss, self.veil_tr_l) self.__connect_regions("Veil left of sun temple", "Octo cave top path", self.veil_tr_l, self.octo_cave_t, @@ -724,7 +724,7 @@ def __connect_abyss_regions(self) -> None: self.__connect_regions("Abyss left area", "Abyss bottom of left area", self.abyss_l, self.abyss_lb, lambda state: _has_nature_form(state, self.player)) - self.__connect_regions("Abyss left bottom area", "Sunken city right area", + self.__connect_regions("Abyss left bottom area", "Sunken City right area", self.abyss_lb, self.sunken_city_r, lambda state: _has_li(state, self.player)) self.__connect_one_way_regions("Abyss left bottom area", "Body center area", @@ -748,13 +748,13 @@ def __connect_abyss_regions(self) -> None: _has_sun_form(state, self.player) and _has_bind_song(state, self.player) and _has_energy_form(state, self.player)) - self.__connect_regions("Abyss right area", "Ice cave", + self.__connect_regions("Abyss right area", "Ice Cave", self.abyss_r, self.ice_cave, lambda state: _has_spirit_form(state, self.player)) - self.__connect_regions("Abyss right area", "Bubble cave", + self.__connect_regions("Abyss right area", "Bubble Cave", self.ice_cave, self.bubble_cave, lambda state: _has_beast_form(state, self.player)) - self.__connect_regions("Bubble cave boss area", "Bubble cave", + self.__connect_regions("Bubble Cave boss area", "Bubble Cave", self.bubble_cave, self.bubble_cave_boss, lambda state: _has_nature_form(state, self.player) and _has_bind_song(state, self.player) ) @@ -763,12 +763,12 @@ def __connect_sunken_city_regions(self) -> None: """ Connect entrances of the different regions around The Sunken City """ - self.__connect_regions("Sunken city right area", "Sunken city left area", + self.__connect_regions("Sunken City right area", "Sunken City left area", self.sunken_city_r, self.sunken_city_l) - self.__connect_regions("Sunken city left area", "Sunken city bedroom", + self.__connect_regions("Sunken City left area", "Sunken City bedroom", self.sunken_city_l, self.sunken_city_l_bedroom, lambda state: _has_spirit_form(state, self.player)) - self.__connect_regions("Sunken city left area", "Sunken city boss area", + self.__connect_regions("Sunken City left area", "Sunken City boss area", self.sunken_city_l, self.sunken_city_boss, lambda state: _has_beast_form(state, self.player) and _has_energy_form(state, self.player) and @@ -776,7 +776,7 @@ def __connect_sunken_city_regions(self) -> None: def __connect_body_regions(self) -> None: """ - Connect entrances of the different regions around The body + Connect entrances of the different regions around The Body """ self.__connect_regions("Body center area", "Body left area", self.body_c, self.body_l) @@ -787,13 +787,13 @@ def __connect_body_regions(self) -> None: self.__connect_regions("Body center area", "Body bottom area", self.body_c, self.body_b, lambda state: _has_dual_form(state, self.player)) - self.__connect_regions("Body bottom area", "Final boss area", + self.__connect_regions("Body bottom area", "Final Boss area", self.body_b, self.final_boss_loby, lambda state: _has_dual_form(state, self.player)) - self.__connect_regions("Before Final boss", "Final boss tube", + self.__connect_regions("Before Final Boss", "Final Boss tube", self.final_boss_loby, self.final_boss_tube, lambda state: _has_nature_form(state, self.player)) - self.__connect_one_way_regions("Before Final boss", "Final boss", + self.__connect_one_way_regions("Before Final Boss", "Final Boss", self.final_boss_loby, self.final_boss, lambda state: _has_energy_form(state, self.player) and _has_dual_form(state, self.player) and @@ -814,7 +814,7 @@ def __connect_transturtle(self, item_source: str, item_target: str, region_sourc def __connect_arnassi_path_transturtle(self, item_source: str, item_target: str, region_source: Region, region_target: Region) -> None: - """Connect the Arnassi ruins transturtle to another one""" + """Connect the Arnassi Ruins transturtle to another one""" self.__connect_one_way_regions(item_source, item_target, region_source, region_target, lambda state: state.has(item_target, self.player) and _has_fish_form(state, self.player)) @@ -825,25 +825,25 @@ def _connect_transturtle_to_other(self, item: str, region: Region) -> None: self.__connect_transturtle(item, "Transturtle Veil top right", region, self.veil_tr_l) self.__connect_transturtle(item, "Transturtle Open Water top right", region, self.openwater_tr_turtle) self.__connect_transturtle(item, "Transturtle Forest bottom left", region, self.forest_bl) - self.__connect_transturtle(item, "Transturtle Home water", region, self.home_water_transturtle) + self.__connect_transturtle(item, "Transturtle Home Water", region, self.home_water_transturtle) self.__connect_transturtle(item, "Transturtle Abyss right", region, self.abyss_r) self.__connect_transturtle(item, "Transturtle Final Boss", region, self.final_boss_tube) - self.__connect_transturtle(item, "Transturtle Simon says", region, self.simon) - self.__connect_transturtle(item, "Transturtle Arnassi ruins", region, self.arnassi_path, - lambda state: state.has("Transturtle Arnassi ruins", self.player) and + self.__connect_transturtle(item, "Transturtle Simon Says", region, self.simon) + self.__connect_transturtle(item, "Transturtle Arnassi Ruins", region, self.arnassi_path, + lambda state: state.has("Transturtle Arnassi Ruins", self.player) and _has_fish_form(state, self.player)) def _connect_arnassi_path_transturtle_to_other(self, item: str, region: Region) -> None: - """Connect the Arnassi ruins transturtle to all others""" + """Connect the Arnassi Ruins transturtle to all others""" self.__connect_arnassi_path_transturtle(item, "Transturtle Veil top left", region, self.veil_tl) self.__connect_arnassi_path_transturtle(item, "Transturtle Veil top right", region, self.veil_tr_l) self.__connect_arnassi_path_transturtle(item, "Transturtle Open Water top right", region, self.openwater_tr_turtle) self.__connect_arnassi_path_transturtle(item, "Transturtle Forest bottom left", region, self.forest_bl) - self.__connect_arnassi_path_transturtle(item, "Transturtle Home water", region, self.home_water_transturtle) + self.__connect_arnassi_path_transturtle(item, "Transturtle Home Water", region, self.home_water_transturtle) self.__connect_arnassi_path_transturtle(item, "Transturtle Abyss right", region, self.abyss_r) self.__connect_arnassi_path_transturtle(item, "Transturtle Final Boss", region, self.final_boss_tube) - self.__connect_arnassi_path_transturtle(item, "Transturtle Simon says", region, self.simon) + self.__connect_arnassi_path_transturtle(item, "Transturtle Simon Says", region, self.simon) def __connect_transturtles(self) -> None: """Connect every transturtle with others""" @@ -851,11 +851,11 @@ def __connect_transturtles(self) -> None: self._connect_transturtle_to_other("Transturtle Veil top right", self.veil_tr_l) self._connect_transturtle_to_other("Transturtle Open Water top right", self.openwater_tr_turtle) self._connect_transturtle_to_other("Transturtle Forest bottom left", self.forest_bl) - self._connect_transturtle_to_other("Transturtle Home water", self.home_water_transturtle) + self._connect_transturtle_to_other("Transturtle Home Water", self.home_water_transturtle) self._connect_transturtle_to_other("Transturtle Abyss right", self.abyss_r) self._connect_transturtle_to_other("Transturtle Final Boss", self.final_boss_tube) - self._connect_transturtle_to_other("Transturtle Simon says", self.simon) - self._connect_arnassi_path_transturtle_to_other("Transturtle Arnassi ruins", self.arnassi_path) + self._connect_transturtle_to_other("Transturtle Simon Says", self.simon) + self._connect_arnassi_path_transturtle_to_other("Transturtle Arnassi Ruins", self.arnassi_path) def connect_regions(self) -> None: """ @@ -907,7 +907,7 @@ def __add_event_big_bosses(self) -> None: def __add_event_mini_bosses(self) -> None: """ - Add every mini bosses (excluding Energy statue and Simon says) + Add every mini bosses (excluding Energy Statue and Simon Says) events to the `world` """ self.__add_event_location(self.home_water_nautilus, @@ -967,100 +967,100 @@ def add_event_locations(self) -> None: def __adjusting_urns_rules(self) -> None: """Since Urns need to be broken, add a damaging item to rules""" - add_rule(self.multiworld.get_location("Open water top right area, first urn in the Mithalas exit", self.player), + add_rule(self.multiworld.get_location("Open Water top right area, first urn in the Mithalas exit", self.player), lambda state: _has_damaging_item(state, self.player)) - add_rule(self.multiworld.get_location("Open water top right area, second urn in the Mithalas exit", self.player), + add_rule(self.multiworld.get_location("Open Water top right area, second urn in the Mithalas exit", self.player), lambda state: _has_damaging_item(state, self.player)) - add_rule(self.multiworld.get_location("Open water top right area, third urn in the Mithalas exit", self.player), + add_rule(self.multiworld.get_location("Open Water top right area, third urn in the Mithalas exit", self.player), lambda state: _has_damaging_item(state, self.player)) - add_rule(self.multiworld.get_location("Mithalas city, first urn in one of the homes", self.player), + add_rule(self.multiworld.get_location("Mithalas City, first urn in one of the homes", self.player), lambda state: _has_damaging_item(state, self.player)) - add_rule(self.multiworld.get_location("Mithalas city, second urn in one of the homes", self.player), + add_rule(self.multiworld.get_location("Mithalas City, second urn in one of the homes", self.player), lambda state: _has_damaging_item(state, self.player)) - add_rule(self.multiworld.get_location("Mithalas city, first urn in the city reserve", self.player), + add_rule(self.multiworld.get_location("Mithalas City, first urn in the city reserve", self.player), lambda state: _has_damaging_item(state, self.player)) - add_rule(self.multiworld.get_location("Mithalas city, second urn in the city reserve", self.player), + add_rule(self.multiworld.get_location("Mithalas City, second urn in the city reserve", self.player), lambda state: _has_damaging_item(state, self.player)) - add_rule(self.multiworld.get_location("Mithalas city, third urn in the city reserve", self.player), + add_rule(self.multiworld.get_location("Mithalas City, third urn in the city reserve", self.player), lambda state: _has_damaging_item(state, self.player)) - add_rule(self.multiworld.get_location("Mithalas city, urn in the cathedral flower tube entrance", self.player), + add_rule(self.multiworld.get_location("Mithalas City, urn in the Cathedral flower tube entrance", self.player), lambda state: _has_damaging_item(state, self.player)) - add_rule(self.multiworld.get_location("Mithalas city castle, urn in the bedroom", self.player), + add_rule(self.multiworld.get_location("Mithalas City Castle, urn in the bedroom", self.player), lambda state: _has_damaging_item(state, self.player)) - add_rule(self.multiworld.get_location("Mithalas city castle, first urn of the single lamp path", self.player), + add_rule(self.multiworld.get_location("Mithalas City Castle, first urn of the single lamp path", self.player), lambda state: _has_damaging_item(state, self.player)) - add_rule(self.multiworld.get_location("Mithalas city castle, second urn of the single lamp path", self.player), + add_rule(self.multiworld.get_location("Mithalas City Castle, second urn of the single lamp path", self.player), lambda state: _has_damaging_item(state, self.player)) - add_rule(self.multiworld.get_location("Mithalas city castle, urn in the bottom room", self.player), + add_rule(self.multiworld.get_location("Mithalas City Castle, urn in the bottom room", self.player), lambda state: _has_damaging_item(state, self.player)) - add_rule(self.multiworld.get_location("Mithalas city castle, first urn on the entrance path", self.player), + add_rule(self.multiworld.get_location("Mithalas City Castle, first urn on the entrance path", self.player), lambda state: _has_damaging_item(state, self.player)) - add_rule(self.multiworld.get_location("Mithalas city castle, second urn on the entrance path", self.player), + add_rule(self.multiworld.get_location("Mithalas City Castle, second urn on the entrance path", self.player), lambda state: _has_damaging_item(state, self.player)) - add_rule(self.multiworld.get_location("Mithalas city, urn inside a home fish pass", self.player), + add_rule(self.multiworld.get_location("Mithalas City, urn inside a home fish pass", self.player), lambda state: _has_damaging_item(state, self.player)) def __adjusting_crates_rules(self) -> None: """Since Crate need to be broken, add a damaging item to rules""" - add_rule(self.multiworld.get_location("Sunken city right area, crate close to the save cristal", self.player), + add_rule(self.multiworld.get_location("Sunken City right area, crate close to the save crystal", self.player), lambda state: _has_damaging_item(state, self.player)) - add_rule(self.multiworld.get_location("Sunken city right area, crate in the left bottom room", self.player), + add_rule(self.multiworld.get_location("Sunken City right area, crate in the left bottom room", self.player), lambda state: _has_damaging_item(state, self.player)) - add_rule(self.multiworld.get_location("Sunken city left area, crate in the little pipe room", self.player), + add_rule(self.multiworld.get_location("Sunken City left area, crate in the little pipe room", self.player), lambda state: _has_damaging_item(state, self.player)) - add_rule(self.multiworld.get_location("Sunken city left area, crate close to the save cristal", self.player), + add_rule(self.multiworld.get_location("Sunken City left area, crate close to the save crystal", self.player), lambda state: _has_damaging_item(state, self.player)) - add_rule(self.multiworld.get_location("Sunken city left area, crate before the bedroom", self.player), + add_rule(self.multiworld.get_location("Sunken City left area, crate before the bedroom", self.player), lambda state: _has_damaging_item(state, self.player)) def __adjusting_soup_rules(self) -> None: """ Modify rules for location that need soup """ - add_rule(self.multiworld.get_location("Turtle cave, Urchin costume", self.player), + add_rule(self.multiworld.get_location("Turtle cave, Urchin Costume", self.player), lambda state: _has_hot_soup(state, self.player) and _has_beast_form(state, self.player)) add_rule(self.multiworld.get_location("Sun Worm path, first cliff bulb", self.player), lambda state: _has_hot_soup(state, self.player) and _has_beast_form(state, self.player)) add_rule(self.multiworld.get_location("Sun Worm path, second cliff bulb", self.player), lambda state: _has_hot_soup(state, self.player) and _has_beast_form(state, self.player)) - add_rule(self.multiworld.get_location("The veil top right area, bulb in the top of the water fall", self.player), + add_rule(self.multiworld.get_location("The Veil top right area, bulb in the top of the waterfall", self.player), lambda state: _has_hot_soup(state, self.player) and _has_beast_form(state, self.player)) def __adjusting_under_rock_location(self) -> None: """ Modify rules implying bind song needed for bulb under rocks """ - add_rule(self.multiworld.get_location("Home water, bulb under the rock in the left path from the verse cave", + add_rule(self.multiworld.get_location("Home Water, bulb under the rock in the left path from the Verse Cave", self.player), lambda state: _has_bind_song(state, self.player)) - add_rule(self.multiworld.get_location("Verse cave left area, bulb under the rock at the end of the path", + add_rule(self.multiworld.get_location("Verse Cave left area, bulb under the rock at the end of the path", self.player), lambda state: _has_bind_song(state, self.player)) - add_rule(self.multiworld.get_location("Naija's home, bulb under the rock at the right of the main path", + add_rule(self.multiworld.get_location("Naija's Home, bulb under the rock at the right of the main path", self.player), lambda state: _has_bind_song(state, self.player)) - add_rule(self.multiworld.get_location("Song cave, bulb under the rock in the path to the singing statues", + add_rule(self.multiworld.get_location("Song Cave, bulb under the rock in the path to the singing statues", self.player), lambda state: _has_bind_song(state, self.player)) - add_rule(self.multiworld.get_location("Song cave, bulb under the rock close to the song door", + add_rule(self.multiworld.get_location("Song Cave, bulb under the rock close to the song door", self.player), lambda state: _has_bind_song(state, self.player)) - add_rule(self.multiworld.get_location("Energy temple second area, bulb under the rock", + add_rule(self.multiworld.get_location("Energy Temple second area, bulb under the rock", self.player), lambda state: _has_bind_song(state, self.player)) - add_rule(self.multiworld.get_location("Open water top left area, bulb under the rock in the right path", + add_rule(self.multiworld.get_location("Open Water top left area, bulb under the rock in the right path", self.player), lambda state: _has_bind_song(state, self.player)) - add_rule(self.multiworld.get_location("Open water top left area, bulb under the rock in the left path", + add_rule(self.multiworld.get_location("Open Water top left area, bulb under the rock in the left path", self.player), lambda state: _has_bind_song(state, self.player)) add_rule(self.multiworld.get_location("Kelp Forest top right area, bulb under the rock in the right path", self.player), lambda state: _has_bind_song(state, self.player)) - add_rule(self.multiworld.get_location("The veil top left area, bulb under the rock in the top right path", + add_rule(self.multiworld.get_location("The Veil top left area, bulb under the rock in the top right path", self.player), lambda state: _has_bind_song(state, self.player)) add_rule(self.multiworld.get_location("Abyss right area, bulb behind the rock in the whale room", self.player), lambda state: _has_bind_song(state, self.player)) add_rule(self.multiworld.get_location("Abyss right area, bulb in the middle path", self.player), lambda state: _has_bind_song(state, self.player)) - add_rule(self.multiworld.get_location("The veil top left area, bulb under the rock in the top right path", + add_rule(self.multiworld.get_location("The Veil top left area, bulb under the rock in the top right path", self.player), lambda state: _has_bind_song(state, self.player)) def __adjusting_light_in_dark_place_rules(self) -> None: - add_rule(self.multiworld.get_location("Kelp forest top right area, Black pearl", self.player), + add_rule(self.multiworld.get_location("Kelp Forest top right area, Black Pearl", self.player), lambda state: _has_light(state, self.player)) - add_rule(self.multiworld.get_location("Kelp forest bottom right area, Odd Container", self.player), + add_rule(self.multiworld.get_location("Kelp Forest bottom right area, Odd Container", self.player), lambda state: _has_light(state, self.player)) add_rule(self.multiworld.get_entrance("Transturtle Veil top left to Transturtle Abyss right", self.player), lambda state: _has_light(state, self.player)) @@ -1070,103 +1070,103 @@ def __adjusting_light_in_dark_place_rules(self) -> None: lambda state: _has_light(state, self.player)) add_rule(self.multiworld.get_entrance("Transturtle Forest bottom left to Transturtle Abyss right", self.player), lambda state: _has_light(state, self.player)) - add_rule(self.multiworld.get_entrance("Transturtle Home water to Transturtle Abyss right", self.player), + add_rule(self.multiworld.get_entrance("Transturtle Home Water to Transturtle Abyss right", self.player), lambda state: _has_light(state, self.player)) add_rule(self.multiworld.get_entrance("Transturtle Final Boss to Transturtle Abyss right", self.player), lambda state: _has_light(state, self.player)) - add_rule(self.multiworld.get_entrance("Transturtle Simon says to Transturtle Abyss right", self.player), + add_rule(self.multiworld.get_entrance("Transturtle Simon Says to Transturtle Abyss right", self.player), lambda state: _has_light(state, self.player)) - add_rule(self.multiworld.get_entrance("Transturtle Arnassi ruins to Transturtle Abyss right", self.player), + add_rule(self.multiworld.get_entrance("Transturtle Arnassi Ruins to Transturtle Abyss right", self.player), lambda state: _has_light(state, self.player)) add_rule(self.multiworld.get_entrance("Body center area to Abyss left bottom area", self.player), lambda state: _has_light(state, self.player)) add_rule(self.multiworld.get_entrance("Veil left of sun temple to Octo cave top path", self.player), lambda state: _has_light(state, self.player)) - add_rule(self.multiworld.get_entrance("Open water bottom right area to Abyss right area", self.player), + add_rule(self.multiworld.get_entrance("Open Water bottom right area to Abyss right area", self.player), lambda state: _has_light(state, self.player)) - add_rule(self.multiworld.get_entrance("Open water bottom left area to Abyss left area", self.player), + add_rule(self.multiworld.get_entrance("Open Water bottom left area to Abyss left area", self.player), lambda state: _has_light(state, self.player)) - add_rule(self.multiworld.get_entrance("Sun temple left area to Sun temple right area", self.player), + add_rule(self.multiworld.get_entrance("Sun Temple left area to Sun Temple right area", self.player), lambda state: _has_light(state, self.player) or _has_sun_crystal(state, self.player)) - add_rule(self.multiworld.get_entrance("Sun temple right area to Sun temple left area", self.player), + add_rule(self.multiworld.get_entrance("Sun Temple right area to Sun Temple left area", self.player), lambda state: _has_light(state, self.player) or _has_sun_crystal(state, self.player)) - add_rule(self.multiworld.get_entrance("Veil left of sun temple to Sun temple left area", self.player), + add_rule(self.multiworld.get_entrance("Veil left of sun temple to Sun Temple left area", self.player), lambda state: _has_light(state, self.player) or _has_sun_crystal(state, self.player)) def __adjusting_manual_rules(self) -> None: - add_rule(self.multiworld.get_location("Mithalas cathedral, Mithalan Dress", self.player), + add_rule(self.multiworld.get_location("Mithalas Cathedral, Mithalan Dress", self.player), lambda state: _has_beast_form(state, self.player)) - add_rule(self.multiworld.get_location("Open water bottom left area, bulb inside the lowest fish pass", self.player), + add_rule(self.multiworld.get_location("Open Water bottom left area, bulb inside the lowest fish pass", self.player), lambda state: _has_fish_form(state, self.player)) - add_rule(self.multiworld.get_location("Kelp forest bottom left area, Walker baby", self.player), + add_rule(self.multiworld.get_location("Kelp Forest bottom left area, Walker baby", self.player), lambda state: _has_spirit_form(state, self.player)) - add_rule(self.multiworld.get_location("The veil top left area, bulb hidden behind the blocking rock", self.player), + add_rule(self.multiworld.get_location("The Veil top left area, bulb hidden behind the blocking rock", self.player), lambda state: _has_bind_song(state, self.player)) add_rule(self.multiworld.get_location("Turtle cave, Turtle Egg", self.player), lambda state: _has_bind_song(state, self.player)) add_rule(self.multiworld.get_location("Abyss left area, bulb in the bottom fish pass", self.player), lambda state: _has_fish_form(state, self.player)) - add_rule(self.multiworld.get_location("Song cave, Anemone seed", self.player), + add_rule(self.multiworld.get_location("Song Cave, Anemone Seed", self.player), lambda state: _has_nature_form(state, self.player)) - add_rule(self.multiworld.get_location("Song cave, Verse egg", self.player), + add_rule(self.multiworld.get_location("Song Cave, Verse Egg", self.player), lambda state: _has_bind_song(state, self.player)) - add_rule(self.multiworld.get_location("Verse cave right area, Big Seed", self.player), + add_rule(self.multiworld.get_location("Verse Cave right area, Big Seed", self.player), lambda state: _has_bind_song(state, self.player)) - add_rule(self.multiworld.get_location("Arnassi ruins, Song plant spore on the top of the ruins", self.player), + add_rule(self.multiworld.get_location("Arnassi Ruins, Song Plant Spore", self.player), lambda state: _has_beast_form(state, self.player)) - add_rule(self.multiworld.get_location("Energy temple first area, bulb in the bottom room blocked by a rock", + add_rule(self.multiworld.get_location("Energy Temple first area, bulb in the bottom room blocked by a rock", self.player), lambda state: _has_energy_form(state, self.player)) - add_rule(self.multiworld.get_location("Home water, bulb in the bottom left room", self.player), + add_rule(self.multiworld.get_location("Home Water, bulb in the bottom left room", self.player), lambda state: _has_bind_song(state, self.player)) - add_rule(self.multiworld.get_location("Home water, bulb in the path below Nautilus Prime", self.player), + add_rule(self.multiworld.get_location("Home Water, bulb in the path below Nautilus Prime", self.player), lambda state: _has_bind_song(state, self.player)) - add_rule(self.multiworld.get_location("Naija's home, bulb after the energy door", self.player), + add_rule(self.multiworld.get_location("Naija's Home, bulb after the energy door", self.player), lambda state: _has_energy_form(state, self.player)) add_rule(self.multiworld.get_location("Abyss right area, bulb behind the rock in the whale room", self.player), lambda state: _has_spirit_form(state, self.player) and _has_sun_form(state, self.player)) - add_rule(self.multiworld.get_location("Arnassi ruins, Arnassi Armor", self.player), + add_rule(self.multiworld.get_location("Arnassi Ruins, Arnassi Armor", self.player), lambda state: _has_fish_form(state, self.player) and _has_spirit_form(state, self.player)) def __no_progression_hard_or_hidden_location(self) -> None: - self.multiworld.get_location("Energy temple boss area, Fallen god tooth", + self.multiworld.get_location("Energy Temple boss area, Fallen God Tooth", self.player).item_rule =\ lambda item: item.classification != ItemClassification.progression self.multiworld.get_location("Cathedral boss area, beating Mithalan God", self.player).item_rule =\ lambda item: item.classification != ItemClassification.progression - self.multiworld.get_location("Kelp forest boss area, beating Drunian God", + self.multiworld.get_location("Kelp Forest boss area, beating Drunian God", self.player).item_rule =\ lambda item: item.classification != ItemClassification.progression - self.multiworld.get_location("Sun temple boss area, beating Sun God", + self.multiworld.get_location("Sun Temple boss area, beating Sun God", self.player).item_rule =\ lambda item: item.classification != ItemClassification.progression - self.multiworld.get_location("Sunken city, bulb on the top of the boss area (boiler room)", + self.multiworld.get_location("Sunken City, bulb on top of the boss area", self.player).item_rule =\ lambda item: item.classification != ItemClassification.progression - self.multiworld.get_location("Home water, Nautilus Egg", + self.multiworld.get_location("Home Water, Nautilus Egg", self.player).item_rule =\ lambda item: item.classification != ItemClassification.progression - self.multiworld.get_location("Energy temple blaster room, Blaster egg", + self.multiworld.get_location("Energy Temple blaster room, Blaster Egg", self.player).item_rule =\ lambda item: item.classification != ItemClassification.progression - self.multiworld.get_location("Mithalas castle, beating the priests", + self.multiworld.get_location("Mithalas City Castle, beating the Priests", self.player).item_rule =\ lambda item: item.classification != ItemClassification.progression self.multiworld.get_location("Mermog cave, Piranha Egg", self.player).item_rule =\ lambda item: item.classification != ItemClassification.progression - self.multiworld.get_location("Octopus cave, Dumbo Egg", + self.multiworld.get_location("Octopus Cave, Dumbo Egg", self.player).item_rule =\ lambda item: item.classification != ItemClassification.progression - self.multiworld.get_location("King Jellyfish cave, bulb in the right path from King Jelly", + self.multiworld.get_location("King Jellyfish Cave, bulb in the right path from King Jelly", self.player).item_rule =\ lambda item: item.classification != ItemClassification.progression - self.multiworld.get_location("King Jellyfish cave, Jellyfish Costume", + self.multiworld.get_location("King Jellyfish Cave, Jellyfish Costume", self.player).item_rule =\ lambda item: item.classification != ItemClassification.progression - self.multiworld.get_location("Final boss area, bulb in the boss third form room", + self.multiworld.get_location("Final Boss area, bulb in the boss third form room", self.player).item_rule =\ lambda item: item.classification != ItemClassification.progression self.multiworld.get_location("Sun Worm path, first cliff bulb", @@ -1175,34 +1175,34 @@ def __no_progression_hard_or_hidden_location(self) -> None: self.multiworld.get_location("Sun Worm path, second cliff bulb", self.player).item_rule =\ lambda item: item.classification != ItemClassification.progression - self.multiworld.get_location("The veil top right area, bulb in the top of the water fall", + self.multiworld.get_location("The Veil top right area, bulb in the top of the waterfall", self.player).item_rule =\ lambda item: item.classification != ItemClassification.progression - self.multiworld.get_location("Bubble cave, bulb in the left cave wall", + self.multiworld.get_location("Bubble Cave, bulb in the left cave wall", self.player).item_rule =\ lambda item: item.classification != ItemClassification.progression - self.multiworld.get_location("Bubble cave, bulb in the right cave wall (behind the ice cristal)", + self.multiworld.get_location("Bubble Cave, bulb in the right cave wall (behind the ice crystal)", self.player).item_rule =\ lambda item: item.classification != ItemClassification.progression - self.multiworld.get_location("Bubble cave, Verse egg", + self.multiworld.get_location("Bubble Cave, Verse Egg", self.player).item_rule =\ lambda item: item.classification != ItemClassification.progression self.multiworld.get_location("Kelp Forest bottom left area, bulb close to the spirit crystals", self.player).item_rule =\ lambda item: item.classification != ItemClassification.progression - self.multiworld.get_location("Kelp forest bottom left area, Walker baby", + self.multiworld.get_location("Kelp Forest bottom left area, Walker baby", self.player).item_rule =\ lambda item: item.classification != ItemClassification.progression - self.multiworld.get_location("Sun temple, Sun key", + self.multiworld.get_location("Sun Temple, Sun Key", self.player).item_rule =\ lambda item: item.classification != ItemClassification.progression - self.multiworld.get_location("The body bottom area, Mutant Costume", + self.multiworld.get_location("The Body bottom area, Mutant Costume", self.player).item_rule =\ lambda item: item.classification != ItemClassification.progression - self.multiworld.get_location("Sun temple, bulb in the hidden room of the right part", + self.multiworld.get_location("Sun Temple, bulb in the hidden room of the right part", self.player).item_rule =\ lambda item: item.classification != ItemClassification.progression - self.multiworld.get_location("Arnassi ruins, Arnassi Armor", + self.multiworld.get_location("Arnassi Ruins, Arnassi Armor", self.player).item_rule =\ lambda item: item.classification != ItemClassification.progression @@ -1220,19 +1220,19 @@ def adjusting_rules(self, options: AquariaOptions) -> None: self.__adjusting_under_rock_location() if options.mini_bosses_to_beat.value > 0: - add_rule(self.multiworld.get_entrance("Before Final boss to Final boss", self.player), + add_rule(self.multiworld.get_entrance("Before Final Boss to Final Boss", self.player), lambda state: _has_mini_bosses(state, self.player)) if options.big_bosses_to_beat.value > 0: - add_rule(self.multiworld.get_entrance("Before Final boss to Final boss", self.player), + add_rule(self.multiworld.get_entrance("Before Final Boss to Final Boss", self.player), lambda state: _has_big_bosses(state, self.player)) if options.objective.value == 1: - add_rule(self.multiworld.get_entrance("Before Final boss to Final boss", self.player), + add_rule(self.multiworld.get_entrance("Before Final Boss to Final Boss", self.player), lambda state: _has_secrets(state, self.player)) if options.unconfine_home_water.value in [0, 1]: - add_rule(self.multiworld.get_entrance("Home Water to Home water transturtle room", self.player), + add_rule(self.multiworld.get_entrance("Home Water to Home Water transturtle room", self.player), lambda state: _has_bind_song(state, self.player)) if options.unconfine_home_water.value in [0, 2]: - add_rule(self.multiworld.get_entrance("Home Water to Open water top left area", self.player), + add_rule(self.multiworld.get_entrance("Home Water to Open Water top left area", self.player), lambda state: _has_bind_song(state, self.player) and _has_energy_form(state, self.player)) if options.early_energy_form: self.multiworld.early_items[self.player]["Energy form"] = 1 diff --git a/worlds/aquaria/__init__.py b/worlds/aquaria/__init__.py index 7c92d33a9c74..3c0cc3bdedca 100644 --- a/worlds/aquaria/__init__.py +++ b/worlds/aquaria/__init__.py @@ -71,9 +71,9 @@ class AquariaWorld(World): item_name_groups = { "Damage": {"Energy form", "Nature form", "Beast form", - "Li and Li song", "Baby nautilus", "Baby piranha", - "Baby blaster"}, - "Light": {"Sun form", "Baby dumbo"} + "Li and Li song", "Baby Nautilus", "Baby Piranha", + "Baby Blaster"}, + "Light": {"Sun form", "Baby Dumbo"} } """Grouping item make it easier to find them""" @@ -152,20 +152,20 @@ def create_items(self) -> None: precollected = [item.name for item in self.multiworld.precollected_items[self.player]] if self.options.turtle_randomizer.value > 0: if self.options.turtle_randomizer.value == 2: - self.__pre_fill_item("Transturtle Final Boss", "Final boss area, Transturtle", precollected) + self.__pre_fill_item("Transturtle Final Boss", "Final Boss area, Transturtle", precollected) else: - self.__pre_fill_item("Transturtle Veil top left", "The veil top left area, Transturtle", precollected) - self.__pre_fill_item("Transturtle Veil top right", "The veil top right area, Transturtle", precollected) - self.__pre_fill_item("Transturtle Open Water top right", "Open water top right area, Transturtle", + self.__pre_fill_item("Transturtle Veil top left", "The Veil top left area, Transturtle", precollected) + self.__pre_fill_item("Transturtle Veil top right", "The Veil top right area, Transturtle", precollected) + self.__pre_fill_item("Transturtle Open Water top right", "Open Water top right area, Transturtle", precollected) self.__pre_fill_item("Transturtle Forest bottom left", "Kelp Forest bottom left area, Transturtle", precollected) - self.__pre_fill_item("Transturtle Home water", "Home water, Transturtle", precollected) + self.__pre_fill_item("Transturtle Home Water", "Home Water, Transturtle", precollected) self.__pre_fill_item("Transturtle Abyss right", "Abyss right area, Transturtle", precollected) - self.__pre_fill_item("Transturtle Final Boss", "Final boss area, Transturtle", precollected) + self.__pre_fill_item("Transturtle Final Boss", "Final Boss area, Transturtle", precollected) # The last two are inverted because in the original game, they are special turtle that communicate directly - self.__pre_fill_item("Transturtle Simon says", "Arnassi Ruins, Transturtle", precollected) - self.__pre_fill_item("Transturtle Arnassi ruins", "Simon says area, Transturtle", precollected) + self.__pre_fill_item("Transturtle Simon Says", "Arnassi Ruins, Transturtle", precollected) + self.__pre_fill_item("Transturtle Arnassi Ruins", "Simon Says area, Transturtle", precollected) for name, data in item_table.items(): if name in precollected: precollected.remove(name) diff --git a/worlds/aquaria/docs/en_Aquaria.md b/worlds/aquaria/docs/en_Aquaria.md index c37f27568d97..c3e5f54dd66a 100644 --- a/worlds/aquaria/docs/en_Aquaria.md +++ b/worlds/aquaria/docs/en_Aquaria.md @@ -15,14 +15,14 @@ The locations in the randomizer are: - All Mithalas Urns - All Sunken City crates - Collectible treasure locations (including pet eggs and costumes) -- Beating Simon says +- Beating Simon Says - Li cave - Every Transportation Turtle (also called transturtle) - Locations where you get songs: - * Erulian spirit cristal + * Erulian spirit crystal * Energy status mini-boss * Beating Mithalan God boss - * Fish cave puzzle + * Fish Cave puzzle * Beating Drunian God boss * Beating Sun God boss * Breaking Li cage in the body @@ -61,4 +61,4 @@ what has been collected and who will receive it. ## When the player receives an item, what happens? When you receive an item, a message will pop up to inform you where you received -the item from and which one it was. \ No newline at end of file +the item from and which one it was. diff --git a/worlds/aquaria/test/__init__.py b/worlds/aquaria/test/__init__.py index ba42ac6d2c51..198ccb0f628b 100644 --- a/worlds/aquaria/test/__init__.py +++ b/worlds/aquaria/test/__init__.py @@ -10,148 +10,148 @@ # Every location accessible after the home water. after_home_water_locations = [ "Sun Crystal", - "Home water, Transturtle", - "Open water top left area, bulb under the rock in the right path", - "Open water top left area, bulb under the rock in the left path", - "Open water top left area, bulb to the right of the save cristal", - "Open water top right area, bulb in the small path before Mithalas", - "Open water top right area, bulb in the path from the left entrance", - "Open water top right area, bulb in the clearing close to the bottom exit", - "Open water top right area, bulb in the big clearing close to the save cristal", - "Open water top right area, bulb in the big clearing to the top exit", - "Open water top right area, first urn in the Mithalas exit", - "Open water top right area, second urn in the Mithalas exit", - "Open water top right area, third urn in the Mithalas exit", - "Open water top right area, bulb in the turtle room", - "Open water top right area, Transturtle", - "Open water bottom left area, bulb behind the chomper fish", - "Open water bottom left area, bulb inside the lowest fish pass", - "Open water skeleton path, bulb close to the right exit", - "Open water skeleton path, bulb behind the chomper fish", - "Open water skeleton path, King skull", + "Home Water, Transturtle", + "Open Water top left area, bulb under the rock in the right path", + "Open Water top left area, bulb under the rock in the left path", + "Open Water top left area, bulb to the right of the save crystal", + "Open Water top right area, bulb in the small path before Mithalas", + "Open Water top right area, bulb in the path from the left entrance", + "Open Water top right area, bulb in the clearing close to the bottom exit", + "Open Water top right area, bulb in the big clearing close to the save crystal", + "Open Water top right area, bulb in the big clearing to the top exit", + "Open Water top right area, first urn in the Mithalas exit", + "Open Water top right area, second urn in the Mithalas exit", + "Open Water top right area, third urn in the Mithalas exit", + "Open Water top right area, bulb in the turtle room", + "Open Water top right area, Transturtle", + "Open Water bottom left area, bulb behind the chomper fish", + "Open Water bottom left area, bulb inside the lowest fish pass", + "Open Water skeleton path, bulb close to the right exit", + "Open Water skeleton path, bulb behind the chomper fish", + "Open Water skeleton path, King Skull", "Arnassi Ruins, bulb in the right part", "Arnassi Ruins, bulb in the left part", "Arnassi Ruins, bulb in the center part", - "Arnassi ruins, Song plant spore on the top of the ruins", - "Arnassi ruins, Arnassi Armor", - "Arnassi Ruins, Arnassi statue", + "Arnassi Ruins, Song Plant Spore", + "Arnassi Ruins, Arnassi Armor", + "Arnassi Ruins, Arnassi Statue", "Arnassi Ruins, Transturtle", - "Arnassi ruins, Crab armor", - "Simon says area, Transturtle", - "Mithalas city, first bulb in the left city part", - "Mithalas city, second bulb in the left city part", - "Mithalas city, bulb in the right part", - "Mithalas city, bulb at the top of the city", - "Mithalas city, first bulb in a broken home", - "Mithalas city, second bulb in a broken home", - "Mithalas city, bulb in the bottom left part", - "Mithalas city, first bulb in one of the homes", - "Mithalas city, second bulb in one of the homes", - "Mithalas city, first urn in one of the homes", - "Mithalas city, second urn in one of the homes", - "Mithalas city, first urn in the city reserve", - "Mithalas city, second urn in the city reserve", - "Mithalas city, third urn in the city reserve", - "Mithalas city, first bulb at the end of the top path", - "Mithalas city, second bulb at the end of the top path", - "Mithalas city, bulb in the top path", - "Mithalas city, Mithalas pot", - "Mithalas city, urn in the cathedral flower tube entrance", - "Mithalas city, Doll", - "Mithalas city, urn inside a home fish pass", - "Mithalas city castle, bulb in the flesh hole", - "Mithalas city castle, Blue banner", - "Mithalas city castle, urn in the bedroom", - "Mithalas city castle, first urn of the single lamp path", - "Mithalas city castle, second urn of the single lamp path", - "Mithalas city castle, urn in the bottom room", - "Mithalas city castle, first urn on the entrance path", - "Mithalas city castle, second urn on the entrance path", - "Mithalas castle, beating the priests", - "Mithalas city castle, Trident head", - "Mithalas cathedral, first urn in the top right room", - "Mithalas cathedral, second urn in the top right room", - "Mithalas cathedral, third urn in the top right room", - "Mithalas cathedral, urn in the flesh room with fleas", - "Mithalas cathedral, first urn in the bottom right path", - "Mithalas cathedral, second urn in the bottom right path", - "Mithalas cathedral, urn behind the flesh vein", - "Mithalas cathedral, urn in the top left eyes boss room", - "Mithalas cathedral, first urn in the path behind the flesh vein", - "Mithalas cathedral, second urn in the path behind the flesh vein", - "Mithalas cathedral, third urn in the path behind the flesh vein", - "Mithalas cathedral, one of the urns in the top right room", - "Mithalas cathedral, Mithalan Dress", - "Mithalas cathedral right area, urn below the left entrance", - "Cathedral underground, bulb in the center part", - "Cathedral underground, first bulb in the top left part", - "Cathedral underground, second bulb in the top left part", - "Cathedral underground, third bulb in the top left part", - "Cathedral underground, bulb close to the save cristal", - "Cathedral underground, bulb in the bottom right path", + "Arnassi Ruins, Crab Armor", + "Simon Says area, Transturtle", + "Mithalas City, first bulb in the left city part", + "Mithalas City, second bulb in the left city part", + "Mithalas City, bulb in the right part", + "Mithalas City, bulb at the top of the city", + "Mithalas City, first bulb in a broken home", + "Mithalas City, second bulb in a broken home", + "Mithalas City, bulb in the bottom left part", + "Mithalas City, first bulb in one of the homes", + "Mithalas City, second bulb in one of the homes", + "Mithalas City, first urn in one of the homes", + "Mithalas City, second urn in one of the homes", + "Mithalas City, first urn in the city reserve", + "Mithalas City, second urn in the city reserve", + "Mithalas City, third urn in the city reserve", + "Mithalas City, first bulb at the end of the top path", + "Mithalas City, second bulb at the end of the top path", + "Mithalas City, bulb in the top path", + "Mithalas City, Mithalas Pot", + "Mithalas City, urn in the Cathedral flower tube entrance", + "Mithalas City, Doll", + "Mithalas City, urn inside a home fish pass", + "Mithalas City Castle, bulb in the flesh hole", + "Mithalas City Castle, Blue banner", + "Mithalas City Castle, urn in the bedroom", + "Mithalas City Castle, first urn of the single lamp path", + "Mithalas City Castle, second urn of the single lamp path", + "Mithalas City Castle, urn in the bottom room", + "Mithalas City Castle, first urn on the entrance path", + "Mithalas City Castle, second urn on the entrance path", + "Mithalas City Castle, beating the Priests", + "Mithalas City Castle, Trident Head", + "Mithalas Cathedral, first urn in the top right room", + "Mithalas Cathedral, second urn in the top right room", + "Mithalas Cathedral, third urn in the top right room", + "Mithalas Cathedral, urn in the flesh room with fleas", + "Mithalas Cathedral, first urn in the bottom right path", + "Mithalas Cathedral, second urn in the bottom right path", + "Mithalas Cathedral, urn behind the flesh vein", + "Mithalas Cathedral, urn in the top left eyes boss room", + "Mithalas Cathedral, first urn in the path behind the flesh vein", + "Mithalas Cathedral, second urn in the path behind the flesh vein", + "Mithalas Cathedral, third urn in the path behind the flesh vein", + "Mithalas Cathedral, fourth urn in the top right room", + "Mithalas Cathedral, Mithalan Dress", + "Mithalas Cathedral right area, urn below the left entrance", + "Cathedral Underground, bulb in the center part", + "Cathedral Underground, first bulb in the top left part", + "Cathedral Underground, second bulb in the top left part", + "Cathedral Underground, third bulb in the top left part", + "Cathedral Underground, bulb close to the save crystal", + "Cathedral Underground, bulb in the bottom right path", "Cathedral boss area, beating Mithalan God", "Kelp Forest top left area, bulb in the bottom left clearing", "Kelp Forest top left area, bulb in the path down from the top left clearing", "Kelp Forest top left area, bulb in the top left clearing", "Kelp Forest top left, Jelly Egg", - "Kelp Forest top left area, bulb close to the Verse egg", - "Kelp forest top left area, Verse egg", + "Kelp Forest top left area, bulb close to the Verse Egg", + "Kelp Forest top left area, Verse Egg", "Kelp Forest top right area, bulb under the rock in the right path", "Kelp Forest top right area, bulb at the left of the center clearing", "Kelp Forest top right area, bulb in the left path's big room", "Kelp Forest top right area, bulb in the left path's small room", "Kelp Forest top right area, bulb at the top of the center clearing", - "Kelp forest top right area, Black pearl", + "Kelp Forest top right area, Black Pearl", "Kelp Forest top right area, bulb in the top fish pass", "Kelp Forest bottom left area, bulb close to the spirit crystals", - "Kelp forest bottom left area, Walker baby", + "Kelp Forest bottom left area, Walker baby", "Kelp Forest bottom left area, Transturtle", - "Kelp forest bottom right area, Odd Container", - "Kelp forest boss area, beating Drunian God", + "Kelp Forest bottom right area, Odd Container", + "Kelp Forest boss area, beating Drunian God", "Kelp Forest boss room, bulb at the bottom of the area", - "Kelp Forest bottom left area, Fish cave puzzle", + "Kelp Forest bottom left area, Fish Cave puzzle", "Kelp Forest sprite cave, bulb inside the fish pass", "Kelp Forest sprite cave, bulb in the second room", - "Kelp Forest Sprite Cave, Seed bag", + "Kelp Forest sprite cave, Seed Bag", "Mermog cave, bulb in the left part of the cave", "Mermog cave, Piranha Egg", - "The veil top left area, In the Li cave", - "The veil top left area, bulb under the rock in the top right path", - "The veil top left area, bulb hidden behind the blocking rock", - "The veil top left area, Transturtle", - "The veil top left area, bulb inside the fish pass", + "The Veil top left area, In Li's cave", + "The Veil top left area, bulb under the rock in the top right path", + "The Veil top left area, bulb hidden behind the blocking rock", + "The Veil top left area, Transturtle", + "The Veil top left area, bulb inside the fish pass", "Turtle cave, Turtle Egg", - "Turtle cave, bulb in bubble cliff", - "Turtle cave, Urchin costume", - "The veil top right area, bulb in the middle of the wall jump cliff", - "The veil top right area, golden starfish at the bottom right of the bottom path", - "The veil top right area, bulb in the top of the water fall", - "The veil top right area, Transturtle", - "The veil bottom area, bulb in the left path", - "The veil bottom area, bulb in the spirit path", - "The veil bottom area, Verse egg", - "The veil bottom area, Stone Head", - "Octopus cave, Dumbo Egg", - "Octopus cave, bulb in the path below the octopus cave path", - "Bubble cave, bulb in the left cave wall", - "Bubble cave, bulb in the right cave wall (behind the ice cristal)", - "Bubble cave, Verse egg", - "Sun temple, bulb in the top left part", - "Sun temple, bulb in the top right part", - "Sun temple, bulb at the top of the high dark room", - "Sun temple, Golden Gear", - "Sun temple, first bulb of the temple", - "Sun temple, bulb on the left part", - "Sun temple, bulb in the hidden room of the right part", - "Sun temple, Sun key", + "Turtle cave, bulb in Bubble Cliff", + "Turtle cave, Urchin Costume", + "The Veil top right area, bulb in the middle of the wall jump cliff", + "The Veil top right area, Golden Starfish", + "The Veil top right area, bulb in the top of the waterfall", + "The Veil top right area, Transturtle", + "The Veil bottom area, bulb in the left path", + "The Veil bottom area, bulb in the spirit path", + "The Veil bottom area, Verse Egg", + "The Veil bottom area, Stone Head", + "Octopus Cave, Dumbo Egg", + "Octopus Cave, bulb in the path below the Octopus Cave path", + "Bubble Cave, bulb in the left cave wall", + "Bubble Cave, bulb in the right cave wall (behind the ice crystal)", + "Bubble Cave, Verse Egg", + "Sun Temple, bulb in the top left part", + "Sun Temple, bulb in the top right part", + "Sun Temple, bulb at the top of the high dark room", + "Sun Temple, Golden Gear", + "Sun Temple, first bulb of the temple", + "Sun Temple, bulb on the left part", + "Sun Temple, bulb in the hidden room of the right part", + "Sun Temple, Sun Key", "Sun Worm path, first path bulb", "Sun Worm path, second path bulb", "Sun Worm path, first cliff bulb", "Sun Worm path, second cliff bulb", - "Sun temple boss area, beating Sun God", + "Sun Temple boss area, beating Sun God", "Abyss left area, bulb in hidden path room", "Abyss left area, bulb in the right part", - "Abyss left area, Glowing seed", + "Abyss left area, Glowing Seed", "Abyss left area, Glowing Plant", "Abyss left area, bulb in the bottom fish pass", "Abyss right area, bulb behind the rock in the whale room", @@ -159,40 +159,40 @@ "Abyss right area, bulb behind the rock in the middle path", "Abyss right area, bulb in the left green room", "Abyss right area, Transturtle", - "Ice cave, bulb in the room to the right", - "Ice cave, First bulbs in the top exit room", - "Ice cave, Second bulbs in the top exit room", - "Ice cave, third bulbs in the top exit room", - "Ice cave, bulb in the left room", - "King Jellyfish cave, bulb in the right path from King Jelly", - "King Jellyfish cave, Jellyfish Costume", - "The whale, Verse egg", - "Sunken city right area, crate close to the save cristal", - "Sunken city right area, crate in the left bottom room", - "Sunken city left area, crate in the little pipe room", - "Sunken city left area, crate close to the save cristal", - "Sunken city left area, crate before the bedroom", - "Sunken city left area, Girl Costume", - "Sunken city, bulb on the top of the boss area (boiler room)", - "The body center area, breaking li cage", - "The body main area, bulb on the main path blocking tube", - "The body left area, first bulb in the top face room", - "The body left area, second bulb in the top face room", - "The body left area, bulb below the water stream", - "The body left area, bulb in the top path to the top face room", - "The body left area, bulb in the bottom face room", - "The body right area, bulb in the top face room", - "The body right area, bulb in the top path to the bottom face room", - "The body right area, bulb in the bottom face room", - "The body bottom area, bulb in the Jelly Zap room", - "The body bottom area, bulb in the nautilus room", - "The body bottom area, Mutant Costume", - "Final boss area, first bulb in the turtle room", - "Final boss area, second bulbs in the turtle room", - "Final boss area, third bulbs in the turtle room", - "Final boss area, Transturtle", - "Final boss area, bulb in the boss third form room", - "Kelp forest, beating Simon says", + "Ice Cave, bulb in the room to the right", + "Ice Cave, first bulb in the top exit room", + "Ice Cave, second bulb in the top exit room", + "Ice Cave, third bulb in the top exit room", + "Ice Cave, bulb in the left room", + "King Jellyfish Cave, bulb in the right path from King Jelly", + "King Jellyfish Cave, Jellyfish Costume", + "The Whale, Verse Egg", + "Sunken City right area, crate close to the save crystal", + "Sunken City right area, crate in the left bottom room", + "Sunken City left area, crate in the little pipe room", + "Sunken City left area, crate close to the save crystal", + "Sunken City left area, crate before the bedroom", + "Sunken City left area, Girl Costume", + "Sunken City, bulb on top of the boss area", + "The Body center area, breaking Li's cage", + "The Body main area, bulb on the main path blocking tube", + "The Body left area, first bulb in the top face room", + "The Body left area, second bulb in the top face room", + "The Body left area, bulb below the water stream", + "The Body left area, bulb in the top path to the top face room", + "The Body left area, bulb in the bottom face room", + "The Body right area, bulb in the top face room", + "The Body right area, bulb in the top path to the bottom face room", + "The Body right area, bulb in the bottom face room", + "The Body bottom area, bulb in the Jelly Zap room", + "The Body bottom area, bulb in the nautilus room", + "The Body bottom area, Mutant Costume", + "Final Boss area, first bulb in the turtle room", + "Final Boss area, second bulb in the turtle room", + "Final Boss area, third bulb in the turtle room", + "Final Boss area, Transturtle", + "Final Boss area, bulb in the boss third form room", + "Simon Says area, beating Simon Says", "Beating Fallen God", "Beating Mithalan God", "Beating Drunian God", diff --git a/worlds/aquaria/test/test_beast_form_access.py b/worlds/aquaria/test/test_beast_form_access.py index a8d5551586a0..c25070d470b5 100644 --- a/worlds/aquaria/test/test_beast_form_access.py +++ b/worlds/aquaria/test/test_beast_form_access.py @@ -13,33 +13,33 @@ class BeastFormAccessTest(AquariaTestBase): def test_beast_form_location(self) -> None: """Test locations that require beast form""" locations = [ - "Mithalas castle, beating the priests", - "Arnassi ruins, Crab armor", - "Arnassi ruins, Song plant spore on the top of the ruins", - "Mithalas city, first bulb at the end of the top path", - "Mithalas city, second bulb at the end of the top path", - "Mithalas city, bulb in the top path", - "Mithalas city, Mithalas pot", - "Mithalas city, urn in the cathedral flower tube entrance", + "Mithalas City Castle, beating the Priests", + "Arnassi Ruins, Crab Armor", + "Arnassi Ruins, Song Plant Spore", + "Mithalas City, first bulb at the end of the top path", + "Mithalas City, second bulb at the end of the top path", + "Mithalas City, bulb in the top path", + "Mithalas City, Mithalas Pot", + "Mithalas City, urn in the Cathedral flower tube entrance", "Mermog cave, Piranha Egg", - "Mithalas cathedral, Mithalan Dress", - "Turtle cave, bulb in bubble cliff", - "Turtle cave, Urchin costume", + "Mithalas Cathedral, Mithalan Dress", + "Turtle cave, bulb in Bubble Cliff", + "Turtle cave, Urchin Costume", "Sun Worm path, first cliff bulb", "Sun Worm path, second cliff bulb", - "The veil top right area, bulb in the top of the water fall", - "Bubble cave, bulb in the left cave wall", - "Bubble cave, bulb in the right cave wall (behind the ice cristal)", - "Bubble cave, Verse egg", - "Sunken city, bulb on the top of the boss area (boiler room)", - "Octopus cave, Dumbo Egg", + "The Veil top right area, bulb in the top of the waterfall", + "Bubble Cave, bulb in the left cave wall", + "Bubble Cave, bulb in the right cave wall (behind the ice crystal)", + "Bubble Cave, Verse Egg", + "Sunken City, bulb on top of the boss area", + "Octopus Cave, Dumbo Egg", "Beating the Golem", "Beating Mergog", "Beating Crabbius Maximus", "Beating Octopus Prime", "Beating Mantis Shrimp Prime", - "King Jellyfish cave, Jellyfish Costume", - "King Jellyfish cave, bulb in the right path from King Jelly", + "King Jellyfish Cave, Jellyfish Costume", + "King Jellyfish Cave, bulb in the right path from King Jelly", "Beating King Jellyfish God Prime", "Beating Mithalan priests", "Sunken City cleared" diff --git a/worlds/aquaria/test/test_bind_song_access.py b/worlds/aquaria/test/test_bind_song_access.py index b137d48ca9c4..ca663369cc63 100644 --- a/worlds/aquaria/test/test_bind_song_access.py +++ b/worlds/aquaria/test/test_bind_song_access.py @@ -17,19 +17,19 @@ class BindSongAccessTest(AquariaTestBase): def test_bind_song_location(self) -> None: """Test locations that require Bind song""" locations = [ - "Verse cave right area, Big Seed", - "Home water, bulb in the path below Nautilus Prime", - "Home water, bulb in the bottom left room", - "Home water, Nautilus Egg", - "Song cave, Verse egg", - "Energy temple first area, beating the energy statue", - "Energy temple first area, bulb in the bottom room blocked by a rock", - "Energy temple first area, Energy Idol", - "Energy temple second area, bulb under the rock", - "Energy temple bottom entrance, Krotite armor", - "Energy temple third area, bulb in the bottom path", - "Energy temple boss area, Fallen god tooth", - "Energy temple blaster room, Blaster egg", + "Verse Cave right area, Big Seed", + "Home Water, bulb in the path below Nautilus Prime", + "Home Water, bulb in the bottom left room", + "Home Water, Nautilus Egg", + "Song Cave, Verse Egg", + "Energy Temple first area, beating the Energy Statue", + "Energy Temple first area, bulb in the bottom room blocked by a rock", + "Energy Temple first area, Energy Idol", + "Energy Temple second area, bulb under the rock", + "Energy Temple bottom entrance, Krotite Armor", + "Energy Temple third area, bulb in the bottom path", + "Energy Temple boss area, Fallen God Tooth", + "Energy Temple blaster room, Blaster Egg", *after_home_water_locations ] items = [["Bind song"]] diff --git a/worlds/aquaria/test/test_bind_song_option_access.py b/worlds/aquaria/test/test_bind_song_option_access.py index 522a064b6251..a75ef60cdf05 100644 --- a/worlds/aquaria/test/test_bind_song_option_access.py +++ b/worlds/aquaria/test/test_bind_song_option_access.py @@ -18,24 +18,24 @@ class BindSongOptionAccessTest(AquariaTestBase): def test_bind_song_location(self) -> None: """Test locations that require Bind song with the bind song needed option activated""" locations = [ - "Verse cave right area, Big Seed", - "Verse cave left area, bulb under the rock at the end of the path", - "Home water, bulb under the rock in the left path from the verse cave", - "Song cave, bulb under the rock close to the song door", - "Song cave, bulb under the rock in the path to the singing statues", - "Naija's home, bulb under the rock at the right of the main path", - "Home water, bulb in the path below Nautilus Prime", - "Home water, bulb in the bottom left room", - "Home water, Nautilus Egg", - "Song cave, Verse egg", - "Energy temple first area, beating the energy statue", - "Energy temple first area, bulb in the bottom room blocked by a rock", - "Energy temple first area, Energy Idol", - "Energy temple second area, bulb under the rock", - "Energy temple bottom entrance, Krotite armor", - "Energy temple third area, bulb in the bottom path", - "Energy temple boss area, Fallen god tooth", - "Energy temple blaster room, Blaster egg", + "Verse Cave right area, Big Seed", + "Verse Cave left area, bulb under the rock at the end of the path", + "Home Water, bulb under the rock in the left path from the Verse Cave", + "Song Cave, bulb under the rock close to the song door", + "Song Cave, bulb under the rock in the path to the singing statues", + "Naija's Home, bulb under the rock at the right of the main path", + "Home Water, bulb in the path below Nautilus Prime", + "Home Water, bulb in the bottom left room", + "Home Water, Nautilus Egg", + "Song Cave, Verse Egg", + "Energy Temple first area, beating the Energy Statue", + "Energy Temple first area, bulb in the bottom room blocked by a rock", + "Energy Temple first area, Energy Idol", + "Energy Temple second area, bulb under the rock", + "Energy Temple bottom entrance, Krotite Armor", + "Energy Temple third area, bulb in the bottom path", + "Energy Temple boss area, Fallen God Tooth", + "Energy Temple blaster room, Blaster Egg", *after_home_water_locations ] items = [["Bind song"]] diff --git a/worlds/aquaria/test/test_confined_home_water.py b/worlds/aquaria/test/test_confined_home_water.py index f4e0e7b67962..72fddfb4048a 100644 --- a/worlds/aquaria/test/test_confined_home_water.py +++ b/worlds/aquaria/test/test_confined_home_water.py @@ -16,5 +16,5 @@ class ConfinedHomeWaterAccessTest(AquariaTestBase): def test_confine_home_water_location(self) -> None: """Test region accessible with confined home water""" - self.assertFalse(self.can_reach_region("Open water top left area"), "Can reach Open water top left area") - self.assertFalse(self.can_reach_region("Home Water, turtle room"), "Can reach Home Water, turtle room") \ No newline at end of file + self.assertFalse(self.can_reach_region("Open Water top left area"), "Can reach Open Water top left area") + self.assertFalse(self.can_reach_region("Home Water, turtle room"), "Can reach Home Water, turtle room") diff --git a/worlds/aquaria/test/test_dual_song_access.py b/worlds/aquaria/test/test_dual_song_access.py index 14c921d7cfeb..8266ffb181d9 100644 --- a/worlds/aquaria/test/test_dual_song_access.py +++ b/worlds/aquaria/test/test_dual_song_access.py @@ -16,10 +16,10 @@ class LiAccessTest(AquariaTestBase): def test_li_song_location(self) -> None: """Test locations that require the dual song""" locations = [ - "The body bottom area, bulb in the Jelly Zap room", - "The body bottom area, bulb in the nautilus room", - "The body bottom area, Mutant Costume", - "Final boss area, bulb in the boss third form room", + "The Body bottom area, bulb in the Jelly Zap room", + "The Body bottom area, bulb in the nautilus room", + "The Body bottom area, Mutant Costume", + "Final Boss area, bulb in the boss third form room", "Objective complete" ] items = [["Dual form"]] diff --git a/worlds/aquaria/test/test_energy_form_access.py b/worlds/aquaria/test/test_energy_form_access.py index edfe8a3f6c19..ce4ed4099416 100644 --- a/worlds/aquaria/test/test_energy_form_access.py +++ b/worlds/aquaria/test/test_energy_form_access.py @@ -17,41 +17,41 @@ class EnergyFormAccessTest(AquariaTestBase): def test_energy_form_location(self) -> None: """Test locations that require Energy form""" locations = [ - "Home water, Nautilus Egg", - "Naija's home, bulb after the energy door", - "Energy temple first area, bulb in the bottom room blocked by a rock", - "Energy temple second area, bulb under the rock", - "Energy temple bottom entrance, Krotite armor", - "Energy temple third area, bulb in the bottom path", - "Energy temple boss area, Fallen god tooth", - "Energy temple blaster room, Blaster egg", - "Mithalas castle, beating the priests", - "Mithalas cathedral, first urn in the top right room", - "Mithalas cathedral, second urn in the top right room", - "Mithalas cathedral, third urn in the top right room", - "Mithalas cathedral, urn in the flesh room with fleas", - "Mithalas cathedral, first urn in the bottom right path", - "Mithalas cathedral, second urn in the bottom right path", - "Mithalas cathedral, urn behind the flesh vein", - "Mithalas cathedral, urn in the top left eyes boss room", - "Mithalas cathedral, first urn in the path behind the flesh vein", - "Mithalas cathedral, second urn in the path behind the flesh vein", - "Mithalas cathedral, third urn in the path behind the flesh vein", - "Mithalas cathedral, one of the urns in the top right room", - "Mithalas cathedral, Mithalan Dress", - "Mithalas cathedral right area, urn below the left entrance", + "Home Water, Nautilus Egg", + "Naija's Home, bulb after the energy door", + "Energy Temple first area, bulb in the bottom room blocked by a rock", + "Energy Temple second area, bulb under the rock", + "Energy Temple bottom entrance, Krotite Armor", + "Energy Temple third area, bulb in the bottom path", + "Energy Temple boss area, Fallen God Tooth", + "Energy Temple blaster room, Blaster Egg", + "Mithalas City Castle, beating the Priests", + "Mithalas Cathedral, first urn in the top right room", + "Mithalas Cathedral, second urn in the top right room", + "Mithalas Cathedral, third urn in the top right room", + "Mithalas Cathedral, urn in the flesh room with fleas", + "Mithalas Cathedral, first urn in the bottom right path", + "Mithalas Cathedral, second urn in the bottom right path", + "Mithalas Cathedral, urn behind the flesh vein", + "Mithalas Cathedral, urn in the top left eyes boss room", + "Mithalas Cathedral, first urn in the path behind the flesh vein", + "Mithalas Cathedral, second urn in the path behind the flesh vein", + "Mithalas Cathedral, third urn in the path behind the flesh vein", + "Mithalas Cathedral, fourth urn in the top right room", + "Mithalas Cathedral, Mithalan Dress", + "Mithalas Cathedral right area, urn below the left entrance", "Cathedral boss area, beating Mithalan God", - "Kelp Forest top left area, bulb close to the Verse egg", - "Kelp forest top left area, Verse egg", - "Kelp forest boss area, beating Drunian God", + "Kelp Forest top left area, bulb close to the Verse Egg", + "Kelp Forest top left area, Verse Egg", + "Kelp Forest boss area, beating Drunian God", "Mermog cave, Piranha Egg", - "Octopus cave, Dumbo Egg", - "Sun temple boss area, beating Sun God", - "Arnassi ruins, Crab armor", - "King Jellyfish cave, bulb in the right path from King Jelly", - "King Jellyfish cave, Jellyfish Costume", - "Sunken city, bulb on the top of the boss area (boiler room)", - "Final boss area, bulb in the boss third form room", + "Octopus Cave, Dumbo Egg", + "Sun Temple boss area, beating Sun God", + "Arnassi Ruins, Crab Armor", + "King Jellyfish Cave, bulb in the right path from King Jelly", + "King Jellyfish Cave, Jellyfish Costume", + "Sunken City, bulb on top of the boss area", + "Final Boss area, bulb in the boss third form room", "Beating Fallen God", "Beating Mithalan God", "Beating Drunian God", @@ -69,4 +69,4 @@ def test_energy_form_location(self) -> None: "Objective complete", ] items = [["Energy form"]] - self.assertAccessDependency(locations, items) \ No newline at end of file + self.assertAccessDependency(locations, items) diff --git a/worlds/aquaria/test/test_fish_form_access.py b/worlds/aquaria/test/test_fish_form_access.py index 30772371721f..d252bb1f1862 100644 --- a/worlds/aquaria/test/test_fish_form_access.py +++ b/worlds/aquaria/test/test_fish_form_access.py @@ -16,22 +16,22 @@ class FishFormAccessTest(AquariaTestBase): def test_fish_form_location(self) -> None: """Test locations that require fish form""" locations = [ - "The veil top left area, bulb inside the fish pass", - "Mithalas city, Doll", - "Mithalas city, urn inside a home fish pass", + "The Veil top left area, bulb inside the fish pass", + "Mithalas City, Doll", + "Mithalas City, urn inside a home fish pass", "Kelp Forest top right area, bulb in the top fish pass", - "The veil bottom area, Verse egg", - "Open water bottom left area, bulb inside the lowest fish pass", - "Kelp Forest top left area, bulb close to the Verse egg", - "Kelp forest top left area, Verse egg", + "The Veil bottom area, Verse Egg", + "Open Water bottom left area, bulb inside the lowest fish pass", + "Kelp Forest top left area, bulb close to the Verse Egg", + "Kelp Forest top left area, Verse Egg", "Mermog cave, bulb in the left part of the cave", "Mermog cave, Piranha Egg", "Beating Mergog", - "Octopus cave, Dumbo Egg", - "Octopus cave, bulb in the path below the octopus cave path", + "Octopus Cave, Dumbo Egg", + "Octopus Cave, bulb in the path below the Octopus Cave path", "Beating Octopus Prime", "Abyss left area, bulb in the bottom fish pass", - "Arnassi ruins, Arnassi Armor" + "Arnassi Ruins, Arnassi Armor" ] items = [["Fish form"]] self.assertAccessDependency(locations, items) diff --git a/worlds/aquaria/test/test_li_song_access.py b/worlds/aquaria/test/test_li_song_access.py index e26d5b5fcd93..42adc90e5aa1 100644 --- a/worlds/aquaria/test/test_li_song_access.py +++ b/worlds/aquaria/test/test_li_song_access.py @@ -16,27 +16,27 @@ class LiAccessTest(AquariaTestBase): def test_li_song_location(self) -> None: """Test locations that require Li""" locations = [ - "Sunken city right area, crate close to the save cristal", - "Sunken city right area, crate in the left bottom room", - "Sunken city left area, crate in the little pipe room", - "Sunken city left area, crate close to the save cristal", - "Sunken city left area, crate before the bedroom", - "Sunken city left area, Girl Costume", - "Sunken city, bulb on the top of the boss area (boiler room)", - "The body center area, breaking li cage", - "The body main area, bulb on the main path blocking tube", - "The body left area, first bulb in the top face room", - "The body left area, second bulb in the top face room", - "The body left area, bulb below the water stream", - "The body left area, bulb in the top path to the top face room", - "The body left area, bulb in the bottom face room", - "The body right area, bulb in the top face room", - "The body right area, bulb in the top path to the bottom face room", - "The body right area, bulb in the bottom face room", - "The body bottom area, bulb in the Jelly Zap room", - "The body bottom area, bulb in the nautilus room", - "The body bottom area, Mutant Costume", - "Final boss area, bulb in the boss third form room", + "Sunken City right area, crate close to the save crystal", + "Sunken City right area, crate in the left bottom room", + "Sunken City left area, crate in the little pipe room", + "Sunken City left area, crate close to the save crystal", + "Sunken City left area, crate before the bedroom", + "Sunken City left area, Girl Costume", + "Sunken City, bulb on top of the boss area", + "The Body center area, breaking Li's cage", + "The Body main area, bulb on the main path blocking tube", + "The Body left area, first bulb in the top face room", + "The Body left area, second bulb in the top face room", + "The Body left area, bulb below the water stream", + "The Body left area, bulb in the top path to the top face room", + "The Body left area, bulb in the bottom face room", + "The Body right area, bulb in the top face room", + "The Body right area, bulb in the top path to the bottom face room", + "The Body right area, bulb in the bottom face room", + "The Body bottom area, bulb in the Jelly Zap room", + "The Body bottom area, bulb in the nautilus room", + "The Body bottom area, Mutant Costume", + "Final Boss area, bulb in the boss third form room", "Beating the Golem", "Sunken City cleared", "Objective complete" diff --git a/worlds/aquaria/test/test_light_access.py b/worlds/aquaria/test/test_light_access.py index 49414e5ace9d..41e65cb30d9b 100644 --- a/worlds/aquaria/test/test_light_access.py +++ b/worlds/aquaria/test/test_light_access.py @@ -20,19 +20,19 @@ def test_light_location(self) -> None: # Since the `assertAccessDependency` sweep for events even if I tell it not to, those location cannot be # tested. # "Third secret", - # "Sun temple, bulb in the top left part", - # "Sun temple, bulb in the top right part", - # "Sun temple, bulb at the top of the high dark room", - # "Sun temple, Golden Gear", + # "Sun Temple, bulb in the top left part", + # "Sun Temple, bulb in the top right part", + # "Sun Temple, bulb at the top of the high dark room", + # "Sun Temple, Golden Gear", # "Sun Worm path, first path bulb", # "Sun Worm path, second path bulb", # "Sun Worm path, first cliff bulb", - "Octopus cave, Dumbo Egg", - "Kelp forest bottom right area, Odd Container", - "Kelp forest top right area, Black pearl", + "Octopus Cave, Dumbo Egg", + "Kelp Forest bottom right area, Odd Container", + "Kelp Forest top right area, Black Pearl", "Abyss left area, bulb in hidden path room", "Abyss left area, bulb in the right part", - "Abyss left area, Glowing seed", + "Abyss left area, Glowing Seed", "Abyss left area, Glowing Plant", "Abyss left area, bulb in the bottom fish pass", "Abyss right area, bulb behind the rock in the whale room", @@ -40,32 +40,32 @@ def test_light_location(self) -> None: "Abyss right area, bulb behind the rock in the middle path", "Abyss right area, bulb in the left green room", "Abyss right area, Transturtle", - "Ice cave, bulb in the room to the right", - "Ice cave, First bulbs in the top exit room", - "Ice cave, Second bulbs in the top exit room", - "Ice cave, third bulbs in the top exit room", - "Ice cave, bulb in the left room", - "Bubble cave, bulb in the left cave wall", - "Bubble cave, bulb in the right cave wall (behind the ice cristal)", - "Bubble cave, Verse egg", + "Ice Cave, bulb in the room to the right", + "Ice Cave, first bulb in the top exit room", + "Ice Cave, second bulb in the top exit room", + "Ice Cave, third bulb in the top exit room", + "Ice Cave, bulb in the left room", + "Bubble Cave, bulb in the left cave wall", + "Bubble Cave, bulb in the right cave wall (behind the ice crystal)", + "Bubble Cave, Verse Egg", "Beating Mantis Shrimp Prime", - "King Jellyfish cave, bulb in the right path from King Jelly", - "King Jellyfish cave, Jellyfish Costume", + "King Jellyfish Cave, bulb in the right path from King Jelly", + "King Jellyfish Cave, Jellyfish Costume", "Beating King Jellyfish God Prime", - "The whale, Verse egg", + "The Whale, Verse Egg", "First secret", - "Sunken city right area, crate close to the save cristal", - "Sunken city right area, crate in the left bottom room", - "Sunken city left area, crate in the little pipe room", - "Sunken city left area, crate close to the save cristal", - "Sunken city left area, crate before the bedroom", - "Sunken city left area, Girl Costume", - "Sunken city, bulb on the top of the boss area (boiler room)", + "Sunken City right area, crate close to the save crystal", + "Sunken City right area, crate in the left bottom room", + "Sunken City left area, crate in the little pipe room", + "Sunken City left area, crate close to the save crystal", + "Sunken City left area, crate before the bedroom", + "Sunken City left area, Girl Costume", + "Sunken City, bulb on top of the boss area", "Sunken City cleared", "Beating the Golem", "Beating Octopus Prime", - "Final boss area, bulb in the boss third form room", + "Final Boss area, bulb in the boss third form room", "Objective complete", ] - items = [["Sun form", "Baby dumbo", "Has sun crystal"]] + items = [["Sun form", "Baby Dumbo", "Has sun crystal"]] self.assertAccessDependency(locations, items) diff --git a/worlds/aquaria/test/test_nature_form_access.py b/worlds/aquaria/test/test_nature_form_access.py index 89e7ceecbbd9..b380e5048fc9 100644 --- a/worlds/aquaria/test/test_nature_form_access.py +++ b/worlds/aquaria/test/test_nature_form_access.py @@ -16,41 +16,41 @@ class NatureFormAccessTest(AquariaTestBase): def test_nature_form_location(self) -> None: """Test locations that require nature form""" locations = [ - "Song cave, Anemone seed", - "Energy temple blaster room, Blaster egg", + "Song Cave, Anemone Seed", + "Energy Temple blaster room, Blaster Egg", "Beating Blaster Peg Prime", - "Kelp forest top left area, Verse egg", - "Kelp Forest top left area, bulb close to the Verse egg", - "Mithalas castle, beating the priests", + "Kelp Forest top left area, Verse Egg", + "Kelp Forest top left area, bulb close to the Verse Egg", + "Mithalas City Castle, beating the Priests", "Kelp Forest sprite cave, bulb in the second room", - "Kelp Forest Sprite Cave, Seed bag", + "Kelp Forest sprite cave, Seed Bag", "Beating Mithalan priests", "Abyss left area, bulb in the bottom fish pass", - "Bubble cave, Verse egg", + "Bubble Cave, Verse Egg", "Beating Mantis Shrimp Prime", - "Sunken city right area, crate close to the save cristal", - "Sunken city right area, crate in the left bottom room", - "Sunken city left area, crate in the little pipe room", - "Sunken city left area, crate close to the save cristal", - "Sunken city left area, crate before the bedroom", - "Sunken city left area, Girl Costume", - "Sunken city, bulb on the top of the boss area (boiler room)", + "Sunken City right area, crate close to the save crystal", + "Sunken City right area, crate in the left bottom room", + "Sunken City left area, crate in the little pipe room", + "Sunken City left area, crate close to the save crystal", + "Sunken City left area, crate before the bedroom", + "Sunken City left area, Girl Costume", + "Sunken City, bulb on top of the boss area", "Beating the Golem", "Sunken City cleared", - "The body center area, breaking li cage", - "The body main area, bulb on the main path blocking tube", - "The body left area, first bulb in the top face room", - "The body left area, second bulb in the top face room", - "The body left area, bulb below the water stream", - "The body left area, bulb in the top path to the top face room", - "The body left area, bulb in the bottom face room", - "The body right area, bulb in the top face room", - "The body right area, bulb in the top path to the bottom face room", - "The body right area, bulb in the bottom face room", - "The body bottom area, bulb in the Jelly Zap room", - "The body bottom area, bulb in the nautilus room", - "The body bottom area, Mutant Costume", - "Final boss area, bulb in the boss third form room", + "The Body center area, breaking Li's cage", + "The Body main area, bulb on the main path blocking tube", + "The Body left area, first bulb in the top face room", + "The Body left area, second bulb in the top face room", + "The Body left area, bulb below the water stream", + "The Body left area, bulb in the top path to the top face room", + "The Body left area, bulb in the bottom face room", + "The Body right area, bulb in the top face room", + "The Body right area, bulb in the top path to the bottom face room", + "The Body right area, bulb in the bottom face room", + "The Body bottom area, bulb in the Jelly Zap room", + "The Body bottom area, bulb in the nautilus room", + "The Body bottom area, Mutant Costume", + "Final Boss area, bulb in the boss third form room", "Objective complete" ] items = [["Nature form"]] diff --git a/worlds/aquaria/test/test_no_progression_hard_hidden_locations.py b/worlds/aquaria/test/test_no_progression_hard_hidden_locations.py index 5876ff31aa0f..817b9547a892 100644 --- a/worlds/aquaria/test/test_no_progression_hard_hidden_locations.py +++ b/worlds/aquaria/test/test_no_progression_hard_hidden_locations.py @@ -15,31 +15,31 @@ class UNoProgressionHardHiddenTest(AquariaTestBase): } unfillable_locations = [ - "Energy temple boss area, Fallen god tooth", + "Energy Temple boss area, Fallen God Tooth", "Cathedral boss area, beating Mithalan God", - "Kelp forest boss area, beating Drunian God", - "Sun temple boss area, beating Sun God", - "Sunken city, bulb on the top of the boss area (boiler room)", - "Home water, Nautilus Egg", - "Energy temple blaster room, Blaster egg", - "Mithalas castle, beating the priests", + "Kelp Forest boss area, beating Drunian God", + "Sun Temple boss area, beating Sun God", + "Sunken City, bulb on top of the boss area", + "Home Water, Nautilus Egg", + "Energy Temple blaster room, Blaster Egg", + "Mithalas City Castle, beating the Priests", "Mermog cave, Piranha Egg", - "Octopus cave, Dumbo Egg", - "King Jellyfish cave, bulb in the right path from King Jelly", - "King Jellyfish cave, Jellyfish Costume", - "Final boss area, bulb in the boss third form room", + "Octopus Cave, Dumbo Egg", + "King Jellyfish Cave, bulb in the right path from King Jelly", + "King Jellyfish Cave, Jellyfish Costume", + "Final Boss area, bulb in the boss third form room", "Sun Worm path, first cliff bulb", "Sun Worm path, second cliff bulb", - "The veil top right area, bulb in the top of the water fall", - "Bubble cave, bulb in the left cave wall", - "Bubble cave, bulb in the right cave wall (behind the ice cristal)", - "Bubble cave, Verse egg", + "The Veil top right area, bulb in the top of the waterfall", + "Bubble Cave, bulb in the left cave wall", + "Bubble Cave, bulb in the right cave wall (behind the ice crystal)", + "Bubble Cave, Verse Egg", "Kelp Forest bottom left area, bulb close to the spirit crystals", - "Kelp forest bottom left area, Walker baby", - "Sun temple, Sun key", - "The body bottom area, Mutant Costume", - "Sun temple, bulb in the hidden room of the right part", - "Arnassi ruins, Arnassi Armor", + "Kelp Forest bottom left area, Walker baby", + "Sun Temple, Sun Key", + "The Body bottom area, Mutant Costume", + "Sun Temple, bulb in the hidden room of the right part", + "Arnassi Ruins, Arnassi Armor", ] def test_unconfine_home_water_both_location_fillable(self) -> None: diff --git a/worlds/aquaria/test/test_progression_hard_hidden_locations.py b/worlds/aquaria/test/test_progression_hard_hidden_locations.py index 6450236097c9..2b7c8ddac93a 100644 --- a/worlds/aquaria/test/test_progression_hard_hidden_locations.py +++ b/worlds/aquaria/test/test_progression_hard_hidden_locations.py @@ -15,31 +15,31 @@ class UNoProgressionHardHiddenTest(AquariaTestBase): } unfillable_locations = [ - "Energy temple boss area, Fallen god tooth", + "Energy Temple boss area, Fallen God Tooth", "Cathedral boss area, beating Mithalan God", - "Kelp forest boss area, beating Drunian God", - "Sun temple boss area, beating Sun God", - "Sunken city, bulb on the top of the boss area (boiler room)", - "Home water, Nautilus Egg", - "Energy temple blaster room, Blaster egg", - "Mithalas castle, beating the priests", + "Kelp Forest boss area, beating Drunian God", + "Sun Temple boss area, beating Sun God", + "Sunken City, bulb on top of the boss area", + "Home Water, Nautilus Egg", + "Energy Temple blaster room, Blaster Egg", + "Mithalas City Castle, beating the Priests", "Mermog cave, Piranha Egg", - "Octopus cave, Dumbo Egg", - "King Jellyfish cave, bulb in the right path from King Jelly", - "King Jellyfish cave, Jellyfish Costume", - "Final boss area, bulb in the boss third form room", + "Octopus Cave, Dumbo Egg", + "King Jellyfish Cave, bulb in the right path from King Jelly", + "King Jellyfish Cave, Jellyfish Costume", + "Final Boss area, bulb in the boss third form room", "Sun Worm path, first cliff bulb", "Sun Worm path, second cliff bulb", - "The veil top right area, bulb in the top of the water fall", - "Bubble cave, bulb in the left cave wall", - "Bubble cave, bulb in the right cave wall (behind the ice cristal)", - "Bubble cave, Verse egg", + "The Veil top right area, bulb in the top of the waterfall", + "Bubble Cave, bulb in the left cave wall", + "Bubble Cave, bulb in the right cave wall (behind the ice crystal)", + "Bubble Cave, Verse Egg", "Kelp Forest bottom left area, bulb close to the spirit crystals", - "Kelp forest bottom left area, Walker baby", - "Sun temple, Sun key", - "The body bottom area, Mutant Costume", - "Sun temple, bulb in the hidden room of the right part", - "Arnassi ruins, Arnassi Armor", + "Kelp Forest bottom left area, Walker baby", + "Sun Temple, Sun Key", + "The Body bottom area, Mutant Costume", + "Sun Temple, bulb in the hidden room of the right part", + "Arnassi Ruins, Arnassi Armor", ] def test_unconfine_home_water_both_location_fillable(self) -> None: diff --git a/worlds/aquaria/test/test_spirit_form_access.py b/worlds/aquaria/test/test_spirit_form_access.py index 4d59d90a4011..a6eec0da5dd3 100644 --- a/worlds/aquaria/test/test_spirit_form_access.py +++ b/worlds/aquaria/test/test_spirit_form_access.py @@ -13,24 +13,24 @@ class SpiritFormAccessTest(AquariaTestBase): def test_spirit_form_location(self) -> None: """Test locations that require spirit form""" locations = [ - "The veil bottom area, bulb in the spirit path", - "Mithalas city castle, Trident head", - "Open water skeleton path, King skull", - "Kelp forest bottom left area, Walker baby", + "The Veil bottom area, bulb in the spirit path", + "Mithalas City Castle, Trident Head", + "Open Water skeleton path, King Skull", + "Kelp Forest bottom left area, Walker baby", "Abyss right area, bulb behind the rock in the whale room", - "The whale, Verse egg", - "Ice cave, bulb in the room to the right", - "Ice cave, First bulbs in the top exit room", - "Ice cave, Second bulbs in the top exit room", - "Ice cave, third bulbs in the top exit room", - "Ice cave, bulb in the left room", - "Bubble cave, bulb in the left cave wall", - "Bubble cave, bulb in the right cave wall (behind the ice cristal)", - "Bubble cave, Verse egg", - "Sunken city left area, Girl Costume", + "The Whale, Verse Egg", + "Ice Cave, bulb in the room to the right", + "Ice Cave, first bulb in the top exit room", + "Ice Cave, second bulb in the top exit room", + "Ice Cave, third bulb in the top exit room", + "Ice Cave, bulb in the left room", + "Bubble Cave, bulb in the left cave wall", + "Bubble Cave, bulb in the right cave wall (behind the ice crystal)", + "Bubble Cave, Verse Egg", + "Sunken City left area, Girl Costume", "Beating Mantis Shrimp Prime", "First secret", - "Arnassi ruins, Arnassi Armor", + "Arnassi Ruins, Arnassi Armor", ] items = [["Spirit form"]] self.assertAccessDependency(locations, items) diff --git a/worlds/aquaria/test/test_sun_form_access.py b/worlds/aquaria/test/test_sun_form_access.py index 159ab717c2ac..dfd732ec910c 100644 --- a/worlds/aquaria/test/test_sun_form_access.py +++ b/worlds/aquaria/test/test_sun_form_access.py @@ -14,11 +14,11 @@ def test_sun_form_location(self) -> None: """Test locations that require sun form""" locations = [ "First secret", - "The whale, Verse egg", + "The Whale, Verse Egg", "Abyss right area, bulb behind the rock in the whale room", - "Octopus cave, Dumbo Egg", + "Octopus Cave, Dumbo Egg", "Beating Octopus Prime", - "Final boss area, bulb in the boss third form room", + "Final Boss area, bulb in the boss third form room", "Objective complete" ] items = [["Sun form"]] diff --git a/worlds/aquaria/test/test_unconfine_home_water_via_both.py b/worlds/aquaria/test/test_unconfine_home_water_via_both.py index 3af17f1b75d1..24d3adad9745 100644 --- a/worlds/aquaria/test/test_unconfine_home_water_via_both.py +++ b/worlds/aquaria/test/test_unconfine_home_water_via_both.py @@ -17,5 +17,5 @@ class UnconfineHomeWaterBothAccessTest(AquariaTestBase): def test_unconfine_home_water_both_location(self) -> None: """Test locations accessible with unconfined home water via energy door and transportation turtle""" - self.assertTrue(self.can_reach_region("Open water top left area"), "Cannot reach Open water top left area") - self.assertTrue(self.can_reach_region("Home Water, turtle room"), "Cannot reach Home Water, turtle room") \ No newline at end of file + self.assertTrue(self.can_reach_region("Open Water top left area"), "Cannot reach Open Water top left area") + self.assertTrue(self.can_reach_region("Home Water, turtle room"), "Cannot reach Home Water, turtle room") diff --git a/worlds/aquaria/test/test_unconfine_home_water_via_energy_door.py b/worlds/aquaria/test/test_unconfine_home_water_via_energy_door.py index bfa82d65eac9..92eb8d029135 100644 --- a/worlds/aquaria/test/test_unconfine_home_water_via_energy_door.py +++ b/worlds/aquaria/test/test_unconfine_home_water_via_energy_door.py @@ -16,5 +16,5 @@ class UnconfineHomeWaterEnergyDoorAccessTest(AquariaTestBase): def test_unconfine_home_water_energy_door_location(self) -> None: """Test locations accessible with unconfined home water via energy door""" - self.assertTrue(self.can_reach_region("Open water top left area"), "Cannot reach Open water top left area") - self.assertFalse(self.can_reach_region("Home Water, turtle room"), "Can reach Home Water, turtle room") \ No newline at end of file + self.assertTrue(self.can_reach_region("Open Water top left area"), "Cannot reach Open Water top left area") + self.assertFalse(self.can_reach_region("Home Water, turtle room"), "Can reach Home Water, turtle room") diff --git a/worlds/aquaria/test/test_unconfine_home_water_via_transturtle.py b/worlds/aquaria/test/test_unconfine_home_water_via_transturtle.py index 627a92db2918..66c40d23f1d8 100644 --- a/worlds/aquaria/test/test_unconfine_home_water_via_transturtle.py +++ b/worlds/aquaria/test/test_unconfine_home_water_via_transturtle.py @@ -17,4 +17,4 @@ class UnconfineHomeWaterTransturtleAccessTest(AquariaTestBase): def test_unconfine_home_water_transturtle_location(self) -> None: """Test locations accessible with unconfined home water via transportation turtle""" self.assertTrue(self.can_reach_region("Home Water, turtle room"), "Cannot reach Home Water, turtle room") - self.assertFalse(self.can_reach_region("Open water top left area"), "Can reach Open water top left area") \ No newline at end of file + self.assertFalse(self.can_reach_region("Open Water top left area"), "Can reach Open Water top left area") From 91c89604a58746e632c91083d10e629be36d9bcc Mon Sep 17 00:00:00 2001 From: Rensen3 <127029481+Rensen3@users.noreply.github.com> Date: Sat, 1 Jun 2024 13:10:02 +0200 Subject: [PATCH 16/27] YGO06: prevent multiple players affecting each others procedure patch (#3409) --- worlds/yugioh06/__init__.py | 6 ++++-- worlds/yugioh06/rom.py | 2 -- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/worlds/yugioh06/__init__.py b/worlds/yugioh06/__init__.py index 2640b13aca4b..1cf44f090fed 100644 --- a/worlds/yugioh06/__init__.py +++ b/worlds/yugioh06/__init__.py @@ -399,12 +399,14 @@ def generate_output(self, output_directory: str): self.playerName.extend([0] * (0x20 - len(self.playerName))) patch = YGO06ProcedurePatch(player=self.player, player_name=self.multiworld.player_name[self.player]) patch.write_file("base_patch.bsdiff4", pkgutil.get_data(__name__, "patch.bsdiff4")) + procedure = [("apply_bsdiff4", ["base_patch.bsdiff4"]), ("apply_tokens", ["token_data.bin"])] if self.is_draft_mode: - patch.procedure.insert(1, ("apply_bsdiff4", ["draft_patch.bsdiff4"])) + procedure.insert(1, ("apply_bsdiff4", ["draft_patch.bsdiff4"])) patch.write_file("draft_patch.bsdiff4", pkgutil.get_data(__name__, "patches/draft.bsdiff4")) if self.options.ocg_arts: - patch.procedure.insert(1, ("apply_bsdiff4", ["ocg_patch.bsdiff4"])) + procedure.insert(1, ("apply_bsdiff4", ["ocg_patch.bsdiff4"])) patch.write_file("ocg_patch.bsdiff4", pkgutil.get_data(__name__, "patches/ocg.bsdiff4")) + patch.procedure = procedure write_tokens(self, patch) # Write Output diff --git a/worlds/yugioh06/rom.py b/worlds/yugioh06/rom.py index 0bd3f1cb7689..3ac10f9ea496 100644 --- a/worlds/yugioh06/rom.py +++ b/worlds/yugioh06/rom.py @@ -22,8 +22,6 @@ class YGO06ProcedurePatch(APProcedurePatch, APTokenMixin): patch_file_ending = ".apygo06" result_file_ending = ".gba" - procedure = [("apply_bsdiff4", ["base_patch.bsdiff4"]), ("apply_tokens", ["token_data.bin"])] - @classmethod def get_source_data(cls) -> bytes: return get_base_rom_bytes() From 67cd32b37c4e5fee871685c3e40bc1bf2470b6a0 Mon Sep 17 00:00:00 2001 From: Bryce Wilson Date: Sat, 1 Jun 2024 04:12:37 -0700 Subject: [PATCH 17/27] Pokemon Emerald: Use `self.player_name` (#3384) --- worlds/pokemon_emerald/__init__.py | 20 ++++++++++---------- worlds/pokemon_emerald/client.py | 2 +- worlds/pokemon_emerald/rom.py | 4 ++-- 3 files changed, 13 insertions(+), 13 deletions(-) diff --git a/worlds/pokemon_emerald/__init__.py b/worlds/pokemon_emerald/__init__.py index 3e50f748d922..aa4f6ccf7519 100644 --- a/worlds/pokemon_emerald/__init__.py +++ b/worlds/pokemon_emerald/__init__.py @@ -174,26 +174,26 @@ def generate_early(self) -> None: # In race mode we don't patch any item location information into the ROM if self.multiworld.is_race and not self.options.remote_items: logging.warning("Pokemon Emerald: Forcing Player %s (%s) to use remote items due to race mode.", - self.player, self.multiworld.player_name[self.player]) + self.player, self.player_name) self.options.remote_items.value = Toggle.option_true if self.options.goal == Goal.option_legendary_hunt: # Prevent turning off all legendary encounters if len(self.options.allowed_legendary_hunt_encounters.value) == 0: - raise OptionError(f"Pokemon Emerald: Player {self.player} ({self.multiworld.player_name[self.player]}) " - "needs to allow at least one legendary encounter when goal is legendary hunt.") + raise OptionError(f"Pokemon Emerald: Player {self.player} ({self.player_name}) needs to allow at " + "least one legendary encounter when goal is legendary hunt.") # Prevent setting the number of required legendaries higher than the number of enabled legendaries if self.options.legendary_hunt_count.value > len(self.options.allowed_legendary_hunt_encounters.value): logging.warning("Pokemon Emerald: Legendary hunt count for Player %s (%s) higher than number of allowed " "legendary encounters. Reducing to number of allowed encounters.", self.player, - self.multiworld.player_name[self.player]) + self.player_name) self.options.legendary_hunt_count.value = len(self.options.allowed_legendary_hunt_encounters.value) # Require random wild encounters if dexsanity is enabled if self.options.dexsanity and self.options.wild_pokemon == RandomizeWildPokemon.option_vanilla: - raise OptionError(f"Pokemon Emerald: Player {self.player} ({self.multiworld.player_name[self.player]}) must " - "not leave wild encounters vanilla if enabling dexsanity.") + raise OptionError(f"Pokemon Emerald: Player {self.player} ({self.player_name}) must not leave wild " + "encounters vanilla if enabling dexsanity.") # If badges or HMs are vanilla, Norman locks you from using Surf, # which means you're not guaranteed to be able to reach Fortree Gym, @@ -223,7 +223,7 @@ def generate_early(self) -> None: if self.options.norman_count.value > max_norman_count: logging.warning("Pokemon Emerald: Norman requirements for Player %s (%s) are unsafe in combination with " - "other settings. Reducing to 4.", self.player, self.multiworld.get_player_name(self.player)) + "other settings. Reducing to 4.", self.player, self.player_name) self.options.norman_count.value = max_norman_count def create_regions(self) -> None: @@ -588,7 +588,7 @@ def generate_output(self, output_directory: str) -> None: randomize_opponent_parties(self) randomize_starters(self) - patch = PokemonEmeraldProcedurePatch(player=self.player, player_name=self.multiworld.player_name[self.player]) + patch = PokemonEmeraldProcedurePatch(player=self.player, player_name=self.player_name) patch.write_file("base_patch.bsdiff4", pkgutil.get_data(__name__, "data/base_patch.bsdiff4")) write_tokens(self, patch) @@ -607,7 +607,7 @@ def write_spoiler(self, spoiler_handle: TextIO): if self.options.dexsanity: from collections import defaultdict - spoiler_handle.write(f"\n\nWild Pokemon ({self.multiworld.player_name[self.player]}):\n\n") + spoiler_handle.write(f"\n\nWild Pokemon ({self.player_name}):\n\n") species_maps = defaultdict(set) for map in self.modified_maps.values(): @@ -669,7 +669,7 @@ def extend_hint_information(self, hint_data): def modify_multidata(self, multidata: Dict[str, Any]): import base64 - multidata["connect_names"][base64.b64encode(self.auth).decode("ascii")] = multidata["connect_names"][self.multiworld.player_name[self.player]] + multidata["connect_names"][base64.b64encode(self.auth).decode("ascii")] = multidata["connect_names"][self.player_name] def fill_slot_data(self) -> Dict[str, Any]: slot_data = self.options.as_dict( diff --git a/worlds/pokemon_emerald/client.py b/worlds/pokemon_emerald/client.py index 3a99a09acb3e..a830957e9c7e 100644 --- a/worlds/pokemon_emerald/client.py +++ b/worlds/pokemon_emerald/client.py @@ -452,7 +452,7 @@ async def handle_death_link(self, ctx: "BizHawkClientContext", guards: Dict[str, self.death_counter = times_whited_out elif times_whited_out > self.death_counter: await ctx.send_death(f"{ctx.player_names[ctx.slot]} is out of usable POKéMON! " - f"{ctx.player_names[ctx.slot]} whited out!") + f"{ctx.player_names[ctx.slot]} whited out!") self.ignore_next_death_link = True self.death_counter = times_whited_out diff --git a/worlds/pokemon_emerald/rom.py b/worlds/pokemon_emerald/rom.py index 09203bab8fc6..968a103ccd25 100644 --- a/worlds/pokemon_emerald/rom.py +++ b/worlds/pokemon_emerald/rom.py @@ -184,7 +184,7 @@ def write_tokens(world: "PokemonEmeraldWorld", patch: PokemonEmeraldProcedurePat location.item.name ) for trainer in alternates) - player_name_ids: Dict[str, int] = {world.multiworld.player_name[world.player]: 0} + player_name_ids: Dict[str, int] = {world.player_name: 0} item_name_offsets: Dict[str, int] = {} next_item_name_offset = 0 for i, (flag, item_player, item_name) in enumerate(sorted(location_info, key=lambda t: t[0])): @@ -208,7 +208,7 @@ def write_tokens(world: "PokemonEmeraldWorld", patch: PokemonEmeraldProcedurePat struct.pack(" Date: Sat, 1 Jun 2024 13:13:00 +0200 Subject: [PATCH 18/27] The Witness: Put Treehouse Both Orange Bridges EP on the normal EPs exclusion list (#3308) --- worlds/witness/data/settings/EP_Shuffle/EP_Easy.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/worlds/witness/data/settings/EP_Shuffle/EP_Easy.txt b/worlds/witness/data/settings/EP_Shuffle/EP_Easy.txt index 6f9c80fc0a94..95c1fc39fb7a 100644 --- a/worlds/witness/data/settings/EP_Shuffle/EP_Easy.txt +++ b/worlds/witness/data/settings/EP_Shuffle/EP_Easy.txt @@ -15,3 +15,4 @@ Disabled Locations: 0x09D63 (Mountain Pink Bridge EP) 0x09D5E (Mountain Blue Bridge EP) 0x09D5D (Mountain Yellow Bridge EP) +0x220BD (Both Orange Bridges EP) From f40b10dc97d9b890a42ce49197e534e33a23d979 Mon Sep 17 00:00:00 2001 From: Bryce Wilson Date: Sat, 1 Jun 2024 04:14:40 -0700 Subject: [PATCH 19/27] Pokemon Emerald: Adjust options (#3278) --- worlds/pokemon_emerald/CHANGELOG.md | 2 + worlds/pokemon_emerald/options.py | 304 ++++++++++++++-------------- 2 files changed, 154 insertions(+), 152 deletions(-) diff --git a/worlds/pokemon_emerald/CHANGELOG.md b/worlds/pokemon_emerald/CHANGELOG.md index f0bed1257739..e967b2039b12 100644 --- a/worlds/pokemon_emerald/CHANGELOG.md +++ b/worlds/pokemon_emerald/CHANGELOG.md @@ -5,6 +5,8 @@ - When you blacklist species from wild encounters and turn on dexsanity, blacklisted species are not added as locations and won't show up in the wild. Previously they would be forced to show up exactly once. - Added support for some new autotracking events. +- Updated option descriptions. +- Added `full` alias for `100` on TM and HM compatibility options. ### Fixes diff --git a/worlds/pokemon_emerald/options.py b/worlds/pokemon_emerald/options.py index 978f9d3dcdc9..e05b5d96ac74 100644 --- a/worlds/pokemon_emerald/options.py +++ b/worlds/pokemon_emerald/options.py @@ -3,7 +3,7 @@ """ from dataclasses import dataclass -from Options import (Choice, DeathLink, DefaultOnToggle, TextChoice, OptionSet, NamedRange, Range, Toggle, FreeText, +from Options import (Choice, DeathLink, DefaultOnToggle, OptionSet, NamedRange, Range, Toggle, FreeText, PerGameCommonOptions) from .data import data @@ -11,12 +11,12 @@ class Goal(Choice): """ - Determines what your goal is to consider the game beaten + Determines what your goal is to consider the game beaten. - Champion: Become the champion and enter the hall of fame - Steven: Defeat Steven in Meteor Falls - Norman: Defeat Norman in Petalburg Gym - Legendary Hunt: Defeat or catch legendary pokemon (or whatever was randomized into their encounters) + - Champion: Become the champion and enter the hall of fame + - Steven: Defeat Steven in Meteor Falls + - Norman: Defeat Norman in Petalburg Gym + - Legendary Hunt: Defeat or catch legendary pokemon (or whatever was randomized into their encounters) """ display_name = "Goal" default = 0 @@ -28,11 +28,11 @@ class Goal(Choice): class RandomizeBadges(Choice): """ - Adds Badges to the pool + Adds Badges to the pool. - Vanilla: Gym leaders give their own badge - Shuffle: Gym leaders give a random badge - Completely Random: Badges can be found anywhere + - Vanilla: Gym leaders give their own badge + - Shuffle: Gym leaders give a random badge + - Completely Random: Badges can be found anywhere """ display_name = "Randomize Badges" default = 2 @@ -43,11 +43,11 @@ class RandomizeBadges(Choice): class RandomizeHms(Choice): """ - Adds HMs to the pool + Adds HMs to the pool. - Vanilla: HMs are at their vanilla locations - Shuffle: HMs are shuffled among vanilla HM locations - Completely Random: HMs can be found anywhere + - Vanilla: HMs are at their vanilla locations + - Shuffle: HMs are shuffled among vanilla HM locations + - Completely Random: HMs can be found anywhere """ display_name = "Randomize HMs" default = 2 @@ -58,50 +58,51 @@ class RandomizeHms(Choice): class RandomizeKeyItems(DefaultOnToggle): """ - Adds most key items to the pool. These are usually required to unlock - a location or region (e.g. Devon Scope, Letter, Basement Key) + Adds most key items to the pool. + + These are usually required to unlock a location or region (e.g. Devon Scope, Letter, Basement Key). """ display_name = "Randomize Key Items" class RandomizeBikes(Toggle): """ - Adds the mach bike and acro bike to the pool + Adds the Mach Bike and Acro Bike to the pool. """ display_name = "Randomize Bikes" class RandomizeEventTickets(Toggle): """ - Adds the event tickets to the pool, which let you access legendaries by sailing from Lilycove + Adds the event tickets to the pool, which let you access legendaries by sailing from Lilycove. """ display_name = "Randomize Event Tickets" class RandomizeRods(Toggle): """ - Adds fishing rods to the pool + Adds fishing rods to the pool. """ display_name = "Randomize Fishing Rods" class RandomizeOverworldItems(DefaultOnToggle): """ - Adds items on the ground with a Pokeball sprite to the pool + Adds items on the ground with a Pokeball sprite to the pool. """ display_name = "Randomize Overworld Items" class RandomizeHiddenItems(Toggle): """ - Adds hidden items to the pool + Adds hidden items to the pool. """ display_name = "Randomize Hidden Items" class RandomizeNpcGifts(Toggle): """ - Adds most gifts received from NPCs to the pool (not including key items or HMs) + Adds most gifts received from NPCs to the pool (not including key items or HMs). """ display_name = "Randomize NPC Gifts" @@ -115,7 +116,9 @@ class RandomizeBerryTrees(Toggle): class Dexsanity(Toggle): """ - Adding a "caught" pokedex entry gives you an item (catching, evolving, trading, etc.). + Adding a "caught" pokedex entry gives you an item (catching, evolving, trading, etc.). Only wild encounters are considered logical access to a species. + + Blacklisting wild encounters removes the dexsanity location. Defeating gym leaders provides dex info, allowing you to see where on the map you can catch species you need. @@ -126,21 +129,20 @@ class Dexsanity(Toggle): class Trainersanity(Toggle): """ - Defeating a trainer for the first time gives you an item. Trainers are no longer missable. + Defeating a trainer gives you an item. - Trainers no longer give you money for winning. Each trainer adds a valuable item (nugget, stardust, etc.) to the pool. + Trainers are no longer missable. Trainers no longer give you money for winning. Each trainer adds a valuable item (Nugget, Stardust, etc.) to the pool. """ display_name = "Trainersanity" class ItemPoolType(Choice): """ - Determines which non-progression items get put into the item pool + Determines which non-progression items get put into the item pool. - Shuffled: Item pool consists of shuffled vanilla items - Diverse Balanced: Item pool consists of random items approximately proportioned - according to what they're replacing (i.e. more pokeballs, fewer X items, etc.) - Diverse: Item pool consists of uniformly random (non-unique) items + - Shuffled: Item pool consists of shuffled vanilla items + - Diverse Balanced: Item pool consists of random items approximately proportioned according to what they're replacing + - Diverse: Item pool consists of uniformly random (non-unique) items """ display_name = "Item Pool Type" default = 0 @@ -151,14 +153,14 @@ class ItemPoolType(Choice): class HiddenItemsRequireItemfinder(DefaultOnToggle): """ - The Itemfinder is logically required to pick up hidden items + The Itemfinder is logically required to pick up hidden items. """ display_name = "Require Itemfinder" class DarkCavesRequireFlash(Choice): """ - Determines whether HM05 Flash is logically required to navigate a dark cave + Determines whether HM05 Flash is logically required to navigate a dark cave. """ display_name = "Require Flash" default = 3 @@ -170,10 +172,10 @@ class DarkCavesRequireFlash(Choice): class EliteFourRequirement(Choice): """ - Sets the requirements to challenge the elite four + Sets the requirements to challenge the elite four. - Badges: Obtain some number of badges - Gyms: Defeat some number of gyms + - Badges: Obtain some number of badges + - Gyms: Defeat some number of gyms """ display_name = "Elite Four Requirement" default = 0 @@ -183,7 +185,7 @@ class EliteFourRequirement(Choice): class EliteFourCount(Range): """ - Sets the number of badges/gyms required to challenge the elite four + Sets the number of badges/gyms required to challenge the elite four. """ display_name = "Elite Four Count" range_start = 0 @@ -193,10 +195,10 @@ class EliteFourCount(Range): class NormanRequirement(Choice): """ - Sets the requirements to challenge the Petalburg Gym + Sets the requirements to challenge the Petalburg Gym. - Badges: Obtain some number of badges - Gyms: Defeat some number of gyms + - Badges: Obtain some number of badges + - Gyms: Defeat some number of gym leaders """ display_name = "Norman Requirement" default = 0 @@ -206,7 +208,7 @@ class NormanRequirement(Choice): class NormanCount(Range): """ - Sets the number of badges/gyms required to challenge the Petalburg Gym + Sets the number of badges/gyms required to challenge the Petalburg Gym. """ display_name = "Norman Count" range_start = 0 @@ -216,14 +218,16 @@ class NormanCount(Range): class LegendaryHuntCatch(Toggle): """ - Sets whether legendaries need to be caught to satisfy the Legendary Hunt win condition. Defeated legendaries can be respawned by defeating the Elite 4. + Sets whether legendaries need to be caught to satisfy the Legendary Hunt win condition. + + Defeated legendaries can be respawned by defeating the Elite 4. """ display_name = "Legendary Hunt Requires Catching" class LegendaryHuntCount(Range): """ - Sets the number of legendaries that must be caught/defeated for the Legendary Hunt goal + Sets the number of legendaries that must be caught/defeated for the Legendary Hunt goal. """ display_name = "Legendary Hunt Count" range_start = 1 @@ -235,24 +239,12 @@ class AllowedLegendaryHuntEncounters(OptionSet): """ Sets which legendary encounters can contribute to the Legendary Hunt goal. - Latios will always be the roamer. Latias will always be at Southern Island. - - Possible values are: - "Groudon" - "Kyogre" - "Rayquaza" - "Latios" - "Latias" - "Regirock" - "Registeel" - "Regice" - "Ho-Oh" - "Lugia" - "Deoxys" - "Mew" + Latias will always be at Southern Island. Latios will always be the roamer. The TV broadcast describing the roamer gives you "seen" info for Latios. + + The braille puzzle in Sealed Chamber gives you "seen" info for Wailord and Relicanth. The move tutor in Fortree City always teaches Dig. """ display_name = "Allowed Legendary Hunt Encounters" - valid_keys = frozenset([ + valid_keys = [ "Groudon", "Kyogre", "Rayquaza", @@ -265,19 +257,19 @@ class AllowedLegendaryHuntEncounters(OptionSet): "Lugia", "Deoxys", "Mew", - ]) + ] default = valid_keys.copy() class RandomizeWildPokemon(Choice): """ - Randomizes wild pokemon encounters (grass, caves, water, fishing) + Randomizes wild pokemon encounters (grass, caves, water, fishing). - Vanilla: Wild encounters are unchanged - Match Base Stats: Wild pokemon are replaced with species with approximately the same bst - Match Type: Wild pokemon are replaced with species that share a type with the original - Match Base Stats and Type: Apply both Match Base Stats and Match Type - Completely Random: There are no restrictions + - Vanilla: Wild encounters are unchanged + - Match Base Stats: Wild pokemon are replaced with species with approximately the same bst + - Match Type: Wild pokemon are replaced with species that share a type with the original + - Match Base Stats and Type: Apply both Match Base Stats and Match Type + - Completely Random: There are no restrictions """ display_name = "Randomize Wild Pokemon" default = 0 @@ -294,21 +286,21 @@ class WildEncounterBlacklist(OptionSet): May be overridden if enforcing other restrictions in combination with this blacklist is impossible. - Use "_Legendaries" as a shortcut for legendary pokemon. + Use "_Legendaries" as a shortcut for all legendary pokemon. """ display_name = "Wild Encounter Blacklist" - valid_keys = frozenset(species.label for species in data.species.values()) | {"_Legendaries"} + valid_keys = ["_Legendaries"] + sorted([species.label for species in data.species.values()]) class RandomizeStarters(Choice): """ - Randomizes the starter pokemon in Professor Birch's bag + Randomizes the starter pokemon in Professor Birch's bag. - Vanilla: Starters are unchanged - Match Base Stats: Starters are replaced with species with approximately the same bst - Match Type: Starters are replaced with species that share a type with the original - Match Base Stats and Type: Apply both Match Base Stats and Match Type - Completely Random: There are no restrictions + - Vanilla: Starters are unchanged + - Match Base Stats: Starters are replaced with species with approximately the same bst + - Match Type: Starters are replaced with species that share a type with the original + - Match Base Stats and Type: Apply both Match Base Stats and Match Type + - Completely Random: There are no restrictions """ display_name = "Randomize Starters" default = 0 @@ -325,21 +317,21 @@ class StarterBlacklist(OptionSet): May be overridden if enforcing other restrictions in combination with this blacklist is impossible. - Use "_Legendaries" as a shortcut for legendary pokemon. + Use "_Legendaries" as a shortcut for all legendary pokemon. """ display_name = "Starter Blacklist" - valid_keys = frozenset(species.label for species in data.species.values()) | {"_Legendaries"} + valid_keys = ["_Legendaries"] + sorted([species.label for species in data.species.values()]) class RandomizeTrainerParties(Choice): """ Randomizes the parties of all trainers. - Vanilla: Parties are unchanged - Match Base Stats: Trainer pokemon are replaced with species with approximately the same bst - Match Type: Trainer pokemon are replaced with species that share a type with the original - Match Base Stats and Type: Apply both Match Base Stats and Match Type - Completely Random: There are no restrictions + - Vanilla: Parties are unchanged + - Match Base Stats: Trainer pokemon are replaced with species with approximately the same bst + - Match Type: Trainer pokemon are replaced with species that share a type with the original + - Match Base Stats and Type: Apply both Match Base Stats and Match Type + - Completely Random: There are no restrictions """ display_name = "Randomize Trainer Parties" default = 0 @@ -356,10 +348,10 @@ class TrainerPartyBlacklist(OptionSet): May be overridden if enforcing other restrictions in combination with this blacklist is impossible. - Use "_Legendaries" as a shortcut for legendary pokemon. + Use "_Legendaries" as a shortcut for all legendary pokemon. """ display_name = "Trainer Party Blacklist" - valid_keys = frozenset(species.label for species in data.species.values()) | {"_Legendaries"} + valid_keys = ["_Legendaries"] + sorted([species.label for species in data.species.values()]) class ForceFullyEvolved(Range): @@ -376,12 +368,12 @@ class RandomizeLegendaryEncounters(Choice): """ Randomizes legendary encounters (Rayquaza, Regice, Latias, etc.). The roamer will always be Latios during legendary hunts. - Vanilla: Legendary encounters are unchanged - Shuffle: Legendary encounters are shuffled between each other - Match Base Stats: Legendary encounters are replaced with species with approximately the same bst - Match Type: Legendary encounters are replaced with species that share a type with the original - Match Base Stats and Type: Apply both Match Base Stats and Match Type - Completely Random: There are no restrictions + - Vanilla: Legendary encounters are unchanged + - Shuffle: Legendary encounters are shuffled between each other + - Match Base Stats: Legendary encounters are replaced with species with approximately the same bst + - Match Type: Legendary encounters are replaced with species that share a type with the original + - Match Base Stats and Type: Apply both Match Base Stats and Match Type + - Completely Random: There are no restrictions """ display_name = "Randomize Legendary Encounters" default = 0 @@ -397,12 +389,12 @@ class RandomizeMiscPokemon(Choice): """ Randomizes non-legendary static encounters. May grow to include other pokemon like trades or gifts. - Vanilla: Species are unchanged - Shuffle: Species are shuffled between each other - Match Base Stats: Species are replaced with species with approximately the same bst - Match Type: Species are replaced with species that share a type with the original - Match Base Stats and Type: Apply both Match Base Stats and Match Type - Completely Random: There are no restrictions + - Vanilla: Species are unchanged + - Shuffle: Species are shuffled between each other + - Match Base Stats: Species are replaced with species with approximately the same bst + - Match Type: Species are replaced with species that share a type with the original + - Match Base Stats and Type: Apply both Match Base Stats and Match Type + - Completely Random: There are no restrictions """ display_name = "Randomize Misc Pokemon" default = 0 @@ -418,10 +410,10 @@ class RandomizeTypes(Choice): """ Randomizes the type(s) of every pokemon. Each species will have the same number of types. - Vanilla: Types are unchanged - Shuffle: Types are shuffled globally for all species (e.g. every Water-type pokemon becomes Fire-type) - Completely Random: Each species has its type(s) randomized - Follow Evolutions: Types are randomized per evolution line instead of per species + - Vanilla: Types are unchanged + - Shuffle: Types are shuffled globally for all species (e.g. every Water-type pokemon becomes Fire-type) + - Completely Random: Each species has its type(s) randomized + - Follow Evolutions: Types are randomized per evolution line instead of per species """ display_name = "Randomize Types" default = 0 @@ -435,10 +427,9 @@ class RandomizeAbilities(Choice): """ Randomizes abilities of every species. Each species will have the same number of abilities. - Vanilla: Abilities are unchanged - Completely Random: Each species has its abilities randomized - Follow Evolutions: Abilities are randomized, but if a pokemon would normally retain its ability - when evolving, the random ability will also be retained + - Vanilla: Abilities are unchanged + - Completely Random: Each species has its abilities randomized + - Follow Evolutions: Abilities are randomized, but if a pokemon would normally retain its ability when evolving, the random ability will also be retained """ display_name = "Randomize Abilities" default = 0 @@ -449,22 +440,21 @@ class RandomizeAbilities(Choice): class AbilityBlacklist(OptionSet): """ - A list of abilities which no pokemon should have if abilities are randomized. - For example, you could exclude Wonder Guard and Arena Trap like this: - ["Wonder Guard", "Arena Trap"] + Prevent species from being given these abilities. + + Has no effect if abilities are not randomized. """ display_name = "Ability Blacklist" - valid_keys = frozenset([ability.label for ability in data.abilities]) + valid_keys = sorted([ability.label for ability in data.abilities]) class LevelUpMoves(Choice): """ - Randomizes the moves a pokemon learns when they reach a level where they would learn a move. - Your starter is guaranteed to have a usable damaging move. + Randomizes the moves a pokemon learns when they reach a level where they would learn a move. Your starter is guaranteed to have a usable damaging move. - Vanilla: Learnset is unchanged - Randomized: Moves are randomized - Start with Four Moves: Moves are randomized and all Pokemon know 4 moves at level 1 + - Vanilla: Learnset is unchanged + - Randomized: Moves are randomized + - Start with Four Moves: Moves are randomized and all Pokemon know 4 moves at level 1 """ display_name = "Level Up Moves" default = 0 @@ -487,8 +477,7 @@ class MoveMatchTypeBias(Range): class MoveNormalTypeBias(Range): """ - After it has been decided that a move will not be forced to match types, sets the probability that a learned move - will be forced to be the Normal type. + After it has been decided that a move will not be forced to match types, sets the probability that a learned move will be forced to be the Normal type. If a move is not forced to be Normal, it will be completely random. """ @@ -500,41 +489,51 @@ class MoveNormalTypeBias(Range): class MoveBlacklist(OptionSet): """ - A list of moves which should be excluded from learnsets, TMs, and move tutors. + Prevents species from learning these moves via learnsets, TMs, and move tutors. + + HM moves are already banned. """ display_name = "Move Blacklist" - valid_keys = frozenset(data.move_labels.keys()) + valid_keys = sorted(data.move_labels.keys()) class HmCompatibility(NamedRange): """ - Sets the percent chance that a given HM is compatible with a species + Sets the percent chance that a given HM is compatible with a species. + + Some opponents like gym leaders are allowed to use HMs. This option can affect the moves they know. """ display_name = "HM Compatibility" default = -1 range_start = 50 range_end = 100 special_range_names = { - "vanilla": -1 + "vanilla": -1, + "full": 100, } class TmTutorCompatibility(NamedRange): """ - Sets the percent chance that a given TM or move tutor is compatible with a species + Sets the percent chance that a given TM or move tutor is compatible with a species. + + Some opponents like gym leaders are allowed to use TMs. This option can affect the moves they know. """ display_name = "TM/Tutor Compatibility" default = -1 range_start = 0 range_end = 100 special_range_names = { - "vanilla": -1 + "vanilla": -1, + "full": 100, } class TmTutorMoves(Toggle): """ - Randomizes the moves taught by TMs and move tutors + Randomizes the moves taught by TMs and move tutors. + + Some opponents like gym leaders are allowed to use TMs. This option can affect the moves they know. """ display_name = "TM/Tutor Moves" @@ -562,7 +561,7 @@ class MinCatchRate(Range): class GuaranteedCatch(Toggle): """ - Every throw is guaranteed to catch a wild pokemon + Every throw is guaranteed to catch a wild pokemon. """ display_name = "Guaranteed Catch" @@ -571,14 +570,16 @@ class NormalizeEncounterRates(Toggle): """ Make every slot on an encounter table approximately equally likely. - This does NOT mean every species is equally likely. But it will make rarer encounters less rare overall. + This does NOT mean each species is equally likely. In the vanilla game, each species may occupy more than one slot, and slots vary in probability. + + Species will still occupy the same slots as vanilla, but the slots will be equally weighted. The minimum encounter rate will be 8% (higher in water). """ display_name = "Normalize Encounter Rates" class ExpModifier(Range): """ - Multiplies gained experience by a percentage + Multiplies gained experience by a percentage. 100 is default 50 is half @@ -593,14 +594,14 @@ class ExpModifier(Range): class BlindTrainers(Toggle): """ - Causes trainers to not start a battle with you unless you talk to them + Trainers will not start a battle with you unless you talk to them. """ display_name = "Blind Trainers" class PurgeSpinners(Toggle): """ - Trainers will rotate in predictable patterns on a set interval instead of randomly and don't turn toward you when you run + Trainers will rotate in predictable patterns on a set interval instead of randomly and don't turn toward you when you run. """ display_name = "Purge Spinners" @@ -613,9 +614,9 @@ class MatchTrainerLevels(Choice): This is a pseudo-replacement for a level cap and makes every trainer battle a fair fight while still allowing you to level up. - Off: The vanilla experience - Additive: The modifier you apply to your team is a flat bonus - Multiplicative: The modifier you apply to your team is a percent bonus + - Off: The vanilla experience + - Additive: The modifier you apply to your team is a flat bonus + - Multiplicative: The modifier you apply to your team is a percent bonus """ display_name = "Match Trainer Levels" default = 0 @@ -629,10 +630,10 @@ class MatchTrainerLevelsBonus(Range): A level bonus (or penalty) to apply to your team when matching an opponent's levels. When the match trainer levels option is "additive", this value is added to your team's levels during a battle. - For example, if this value is 5 (+5 levels), you'll have a level 25 team against a level 20 team, and a level 45 team against a level 40 team. + For example, if this value is 5 (+5 levels), you'll have a level 25 team against a level 20 team, and a level 45 team against a level 40 team. When the match trainer levels option is "multiplicative", this is a percent bonus. - For example, if this value is 5 (+5%), you'll have a level 21 team against a level 20 team, and a level 42 team against a level 40 team. + For example, if this value is 5 (+5%), you'll have a level 21 team against a level 20 team, and a level 42 team against a level 40 team. """ display_name = "Match Trainer Levels Modifier" range_start = -100 @@ -643,7 +644,9 @@ class MatchTrainerLevelsBonus(Range): class DoubleBattleChance(Range): """ The percent chance that a trainer with more than 1 pokemon will be converted into a double battle. + If these trainers would normally approach you, they will only do so if you have 2 unfainted pokemon. + They can be battled by talking to them no matter what. """ display_name = "Double Battle Chance" @@ -654,7 +657,7 @@ class DoubleBattleChance(Range): class BetterShops(Toggle): """ - Pokemarts sell every item that can be obtained in a pokemart (except mail, which is still unique to the relevant city) + Pokemarts sell every item that can be obtained in a pokemart (except mail, which is still unique to the relevant city). """ display_name = "Better Shops" @@ -663,19 +666,10 @@ class RemoveRoadblocks(OptionSet): """ Removes specific NPCs that normally stand in your way until certain events are completed. - This can open up the world a bit and make your playthrough less linear, but careful how many you remove; it may make too much of your world accessible upon receiving Surf. - - Possible values are: - "Route 110 Aqua Grunts" - "Route 112 Magma Grunts" - "Route 119 Aqua Grunts" - "Safari Zone Construction Workers" - "Lilycove City Wailmer" - "Aqua Hideout Grunts" - "Seafloor Cavern Aqua Grunt" + This can open up the world a bit and make your playthrough less linear, but be careful how many you remove; it may make too much of your world accessible upon receiving Surf. """ display_name = "Remove Roadblocks" - valid_keys = frozenset([ + valid_keys = [ "Route 110 Aqua Grunts", "Route 112 Magma Grunts", "Route 119 Aqua Grunts", @@ -683,12 +677,13 @@ class RemoveRoadblocks(OptionSet): "Lilycove City Wailmer", "Aqua Hideout Grunts", "Seafloor Cavern Aqua Grunt", - ]) + ] class ExtraBoulders(Toggle): """ Places strength boulders on Route 115 which block access to Meteor Falls from the beach. + This aims to take some power away from Surf by restricting how much it allows you to access. """ display_name = "Extra Boulders" @@ -697,6 +692,7 @@ class ExtraBoulders(Toggle): class ExtraBumpySlope(Toggle): """ Adds a bumpy slope to Route 115 which allows access to Meteor Falls if you have the Acro Bike. + This aims to take some power away from Surf by adding a new way to exit the Rustboro area. """ display_name = "Extra Bumpy Slope" @@ -705,6 +701,7 @@ class ExtraBumpySlope(Toggle): class ModifyRoute118(Toggle): """ Changes the layout of Route 118 so that it must be crossed with the Acro Bike instead of Surf. + This aims to take some power away from Surf by restricting how much it allows you to access. """ display_name = "Modify Route 118" @@ -712,14 +709,14 @@ class ModifyRoute118(Toggle): class FreeFlyLocation(Toggle): """ - Enables flying to one random location when Mom gives you the running shoes (excluding cities reachable with no items) + Enables flying to one random location (excluding cities reachable with no items). """ display_name = "Free Fly Location" -class HmRequirements(TextChoice): +class HmRequirements(Choice): """ - Sets the requirements to use HMs outside of battle + Sets the requirements to use HMs outside of battle. """ display_name = "HM Requirements" default = 0 @@ -729,7 +726,7 @@ class HmRequirements(TextChoice): class TurboA(Toggle): """ - Holding A will advance most text automatically + Holding A will advance most text automatically. """ display_name = "Turbo A" @@ -738,9 +735,9 @@ class ReceiveItemMessages(Choice): """ Determines whether you receive an in-game notification when receiving an item. Items can still only be received in the overworld. - All: Every item shows a message - Progression: Only progression items show a message - None: All items are added to your bag silently (badges will still show) + - All: Every item shows a message + - Progression: Only progression items show a message + - None: All items are added to your bag silently (badges will still show). """ display_name = "Receive Item Messages" default = 0 @@ -754,6 +751,7 @@ class RemoteItems(Toggle): Instead of placing your own items directly into the ROM, all items are received from the server, including items you find for yourself. This enables co-op of a single slot and recovering more items after a lost save file (if you're so unlucky). + But it changes pickup behavior slightly and requires connection to the server to receive any items. """ display_name = "Remote Items" @@ -781,9 +779,10 @@ class WonderTrading(DefaultOnToggle): Wonder trading NEVER affects logic. - Certain aspects of a pokemon species are per-game, not per-pokemon. - As a result, some things are not retained during a trade, including type, ability, level up learnset, and so on. + Certain aspects of a pokemon species are per-game, not per-pokemon. As a result, some things are not retained during a trade, including type, ability, level up learnset, and so on. + Receiving a pokemon this way does not mark it as found in your pokedex. + Trade evolutions do not evolve this way; they retain their modified methods (level ups and item use). """ display_name = "Wonder Trading" @@ -795,6 +794,7 @@ class EasterEgg(FreeText): All secret phrases are something that could be a trendy phrase in Dewford Town. They are case insensitive. """ + display_name = "Easter Egg" default = "EMERALD SECRET" From 4e5b6bb3d23277aad83865f2c909e627ca64f2c2 Mon Sep 17 00:00:00 2001 From: Silvris <58583688+Silvris@users.noreply.github.com> Date: Sat, 1 Jun 2024 06:34:41 -0500 Subject: [PATCH 20/27] Core: move PlandoConnections and PlandoTexts to the options system (#2904) Co-authored-by: Doug Hoskisson Co-authored-by: Scipio Wright Co-authored-by: beauxq Co-authored-by: alwaysintreble --- Generate.py | 29 +- Options.py | 226 +++++++++++++++- worlds/alttp/Options.py | 27 +- worlds/alttp/Rom.py | 4 +- worlds/alttp/Shops.py | 10 +- worlds/alttp/Text.py | 409 +++++++++++++++++++++++++++++ worlds/generic/__init__.py | 6 - worlds/kdl3/Options.py | 7 +- worlds/kdl3/Regions.py | 4 +- worlds/kdl3/test/__init__.py | 3 +- worlds/kdl3/test/test_locations.py | 6 +- worlds/messenger/options.py | 37 ++- worlds/messenger/portals.py | 9 +- worlds/minecraft/Options.py | 15 +- worlds/oot/Options.py | 9 +- worlds/oot/__init__.py | 4 +- worlds/tunic/__init__.py | 14 +- worlds/tunic/er_scripts.py | 4 +- worlds/tunic/options.py | 15 +- 19 files changed, 767 insertions(+), 71 deletions(-) diff --git a/Generate.py b/Generate.py index fab34c893ae9..67988bf8b30d 100644 --- a/Generate.py +++ b/Generate.py @@ -23,9 +23,7 @@ from settings import get_settings from Utils import parse_yamls, version_tuple, __version__, tuplize_version from worlds.alttp.EntranceRandomizer import parse_arguments -from worlds.alttp.Text import TextTable from worlds.AutoWorld import AutoWorldRegister -from worlds.generic import PlandoConnection from worlds import failed_world_loads @@ -506,35 +504,12 @@ def roll_settings(weights: dict, plando_options: PlandoOptions = PlandoOptions.b if PlandoOptions.items in plando_options: ret.plando_items = game_weights.get("plando_items", []) if ret.game == "A Link to the Past": - roll_alttp_settings(ret, game_weights, plando_options) - if PlandoOptions.connections in plando_options: - ret.plando_connections = [] - options = game_weights.get("plando_connections", []) - for placement in options: - if roll_percentage(get_choice("percentage", placement, 100)): - ret.plando_connections.append(PlandoConnection( - get_choice("entrance", placement), - get_choice("exit", placement), - get_choice("direction", placement, "both") - )) + roll_alttp_settings(ret, game_weights) return ret -def roll_alttp_settings(ret: argparse.Namespace, weights, plando_options): - - ret.plando_texts = {} - if PlandoOptions.texts in plando_options: - tt = TextTable() - tt.removeUnwantedText() - options = weights.get("plando_texts", []) - for placement in options: - if roll_percentage(get_choice_legacy("percentage", placement, 100)): - at = str(get_choice_legacy("at", placement)) - if at not in tt: - raise Exception(f"No text target \"{at}\" found.") - ret.plando_texts[at] = str(get_choice_legacy("text", placement)) - +def roll_alttp_settings(ret: argparse.Namespace, weights): ret.sprite_pool = weights.get('sprite_pool', []) ret.sprite = get_choice_legacy('sprite', weights, "Link") if 'random_sprite_on_event' in weights: diff --git a/Options.py b/Options.py index 995473243959..40a6996d325a 100644 --- a/Options.py +++ b/Options.py @@ -12,6 +12,7 @@ from dataclasses import dataclass from schema import And, Optional, Or, Schema +from typing_extensions import Self from Utils import get_fuzzy_results, is_iterable_except_str @@ -896,6 +897,228 @@ class ItemSet(OptionSet): convert_name_groups = True +class PlandoText(typing.NamedTuple): + at: str + text: typing.List[str] + percentage: int = 100 + + +PlandoTextsFromAnyType = typing.Union[ + typing.Iterable[typing.Union[typing.Mapping[str, typing.Any], PlandoText, typing.Any]], typing.Any +] + + +class PlandoTexts(Option[typing.List[PlandoText]], VerifyKeys): + default = () + supports_weighting = False + display_name = "Plando Texts" + + def __init__(self, value: typing.Iterable[PlandoText]) -> None: + self.value = list(deepcopy(value)) + super().__init__() + + def verify(self, world: typing.Type[World], player_name: str, plando_options: "PlandoOptions") -> None: + from BaseClasses import PlandoOptions + if self.value and not (PlandoOptions.texts & plando_options): + # plando is disabled but plando options were given so overwrite the options + self.value = [] + logging.warning(f"The plando texts module is turned off, " + f"so text for {player_name} will be ignored.") + + @classmethod + def from_any(cls, data: PlandoTextsFromAnyType) -> Self: + texts: typing.List[PlandoText] = [] + if isinstance(data, typing.Iterable): + for text in data: + if isinstance(text, typing.Mapping): + if random.random() < float(text.get("percentage", 100)/100): + at = text.get("at", None) + if at is not None: + given_text = text.get("text", []) + if isinstance(given_text, str): + given_text = [given_text] + texts.append(PlandoText( + at, + given_text, + text.get("percentage", 100) + )) + elif isinstance(text, PlandoText): + if random.random() < float(text.percentage/100): + texts.append(text) + else: + raise Exception(f"Cannot create plando text from non-dictionary type, got {type(text)}") + cls.verify_keys([text.at for text in texts]) + return cls(texts) + else: + raise NotImplementedError(f"Cannot Convert from non-list, got {type(data)}") + + @classmethod + def get_option_name(cls, value: typing.List[PlandoText]) -> str: + return str({text.at: " ".join(text.text) for text in value}) + + def __iter__(self) -> typing.Iterator[PlandoText]: + yield from self.value + + def __getitem__(self, index: typing.SupportsIndex) -> PlandoText: + return self.value.__getitem__(index) + + def __len__(self) -> int: + return self.value.__len__() + + +class ConnectionsMeta(AssembleOptions): + def __new__(mcs, name: str, bases: tuple[type, ...], attrs: dict[str, typing.Any]): + if name != "PlandoConnections": + assert "entrances" in attrs, f"Please define valid entrances for {name}" + attrs["entrances"] = frozenset((connection.lower() for connection in attrs["entrances"])) + assert "exits" in attrs, f"Please define valid exits for {name}" + attrs["exits"] = frozenset((connection.lower() for connection in attrs["exits"])) + if "__doc__" not in attrs: + attrs["__doc__"] = PlandoConnections.__doc__ + cls = super().__new__(mcs, name, bases, attrs) + return cls + + +class PlandoConnection(typing.NamedTuple): + class Direction: + entrance = "entrance" + exit = "exit" + both = "both" + + entrance: str + exit: str + direction: typing.Literal["entrance", "exit", "both"] # TODO: convert Direction to StrEnum once 3.8 is dropped + percentage: int = 100 + + +PlandoConFromAnyType = typing.Union[ + typing.Iterable[typing.Union[typing.Mapping[str, typing.Any], PlandoConnection, typing.Any]], typing.Any +] + + +class PlandoConnections(Option[typing.List[PlandoConnection]], metaclass=ConnectionsMeta): + """Generic connections plando. Format is: + - entrance: "Entrance Name" + exit: "Exit Name" + direction: "Direction" + percentage: 100 + Direction must be one of 'entrance', 'exit', or 'both', and defaults to 'both' if omitted. + Percentage is an integer from 1 to 100, and defaults to 100 when omitted.""" + + display_name = "Plando Connections" + + default = () + supports_weighting = False + + entrances: typing.ClassVar[typing.AbstractSet[str]] + exits: typing.ClassVar[typing.AbstractSet[str]] + + duplicate_exits: bool = False + """Whether or not exits should be allowed to be duplicate.""" + + def __init__(self, value: typing.Iterable[PlandoConnection]): + self.value = list(deepcopy(value)) + super(PlandoConnections, self).__init__() + + @classmethod + def validate_entrance_name(cls, entrance: str) -> bool: + return entrance.lower() in cls.entrances + + @classmethod + def validate_exit_name(cls, exit: str) -> bool: + return exit.lower() in cls.exits + + @classmethod + def can_connect(cls, entrance: str, exit: str) -> bool: + """Checks that a given entrance can connect to a given exit. + By default, this will always return true unless overridden.""" + return True + + @classmethod + def validate_plando_connections(cls, connections: typing.Iterable[PlandoConnection]) -> None: + used_entrances: typing.List[str] = [] + used_exits: typing.List[str] = [] + for connection in connections: + entrance = connection.entrance + exit = connection.exit + direction = connection.direction + if direction not in (PlandoConnection.Direction.entrance, + PlandoConnection.Direction.exit, + PlandoConnection.Direction.both): + raise ValueError(f"Unknown direction: {direction}") + if entrance in used_entrances: + raise ValueError(f"Duplicate Entrance {entrance} not allowed.") + if not cls.duplicate_exits and exit in used_exits: + raise ValueError(f"Duplicate Exit {exit} not allowed.") + used_entrances.append(entrance) + used_exits.append(exit) + if not cls.validate_entrance_name(entrance): + raise ValueError(f"{entrance.title()} is not a valid entrance.") + if not cls.validate_exit_name(exit): + raise ValueError(f"{exit.title()} is not a valid exit.") + if not cls.can_connect(entrance, exit): + raise ValueError(f"Connection between {entrance.title()} and {exit.title()} is invalid.") + + @classmethod + def from_any(cls, data: PlandoConFromAnyType) -> Self: + if not isinstance(data, typing.Iterable): + raise Exception(f"Cannot create plando connections from non-List value, got {type(data)}.") + + value: typing.List[PlandoConnection] = [] + for connection in data: + if isinstance(connection, typing.Mapping): + percentage = connection.get("percentage", 100) + if random.random() < float(percentage / 100): + entrance = connection.get("entrance", None) + if is_iterable_except_str(entrance): + entrance = random.choice(sorted(entrance)) + exit = connection.get("exit", None) + if is_iterable_except_str(exit): + exit = random.choice(sorted(exit)) + direction = connection.get("direction", "both") + + if not entrance or not exit: + raise Exception("Plando connection must have an entrance and an exit.") + value.append(PlandoConnection( + entrance, + exit, + direction, + percentage + )) + elif isinstance(connection, PlandoConnection): + if random.random() < float(connection.percentage / 100): + value.append(connection) + else: + raise Exception(f"Cannot create connection from non-Dict type, got {type(connection)}.") + cls.validate_plando_connections(value) + return cls(value) + + def verify(self, world: typing.Type[World], player_name: str, plando_options: "PlandoOptions") -> None: + from BaseClasses import PlandoOptions + if self.value and not (PlandoOptions.connections & plando_options): + # plando is disabled but plando options were given so overwrite the options + self.value = [] + logging.warning(f"The plando connections module is turned off, " + f"so connections for {player_name} will be ignored.") + + @classmethod + def get_option_name(cls, value: typing.List[PlandoConnection]) -> str: + return ", ".join(["%s %s %s" % (connection.entrance, + "<=>" if connection.direction == PlandoConnection.Direction.both else + "<=" if connection.direction == PlandoConnection.Direction.exit else + "=>", + connection.exit) for connection in value]) + + def __getitem__(self, index: typing.SupportsIndex) -> PlandoConnection: + return self.value.__getitem__(index) + + def __iter__(self) -> typing.Iterator[PlandoConnection]: + yield from self.value + + def __len__(self) -> int: + return len(self.value) + + class Accessibility(Choice): """Set rules for reachability of your items/locations. Locations: ensure everything can be reached and acquired. @@ -1049,7 +1272,8 @@ class ItemLinks(OptionList): ]) @staticmethod - def verify_items(items: typing.List[str], item_link: str, pool_name: str, world, allow_item_groups: bool = True) -> typing.Set: + def verify_items(items: typing.List[str], item_link: str, pool_name: str, world, + allow_item_groups: bool = True) -> typing.Set: pool = set() for item_name in items: if item_name not in world.item_names and (not allow_item_groups or item_name not in world.item_name_groups): diff --git a/worlds/alttp/Options.py b/worlds/alttp/Options.py index 8cb377b7a44f..11c1a0165b53 100644 --- a/worlds/alttp/Options.py +++ b/worlds/alttp/Options.py @@ -1,8 +1,11 @@ import typing from BaseClasses import MultiWorld -from Options import Choice, Range, Option, Toggle, DefaultOnToggle, DeathLink, StartInventoryPool, PlandoBosses,\ - FreeText, Removed +from Options import Choice, Range, Option, Toggle, DefaultOnToggle, DeathLink, \ + StartInventoryPool, PlandoBosses, PlandoConnections, PlandoTexts, FreeText, Removed +from .EntranceShuffle import default_connections, default_dungeon_connections, \ + inverted_default_connections, inverted_default_dungeon_connections +from .Text import TextTable class GlitchesRequired(Choice): @@ -721,7 +724,27 @@ class AllowCollect(DefaultOnToggle): display_name = "Allow Collection of checks for other players" +class ALttPPlandoConnections(PlandoConnections): + entrances = set([connection[0] for connection in ( + *default_connections, *default_dungeon_connections, *inverted_default_connections, + *inverted_default_dungeon_connections)]) + exits = set([connection[1] for connection in ( + *default_connections, *default_dungeon_connections, *inverted_default_connections, + *inverted_default_dungeon_connections)]) + + +class ALttPPlandoTexts(PlandoTexts): + """Text plando. Format is: + - text: 'This is your text' + at: text_key + percentage: 100 + Percentage is an integer from 1 to 100, and defaults to 100 when omitted.""" + valid_keys = TextTable.valid_keys + + alttp_options: typing.Dict[str, type(Option)] = { + "plando_connections": ALttPPlandoConnections, + "plando_texts": ALttPPlandoTexts, "start_inventory_from_pool": StartInventoryPool, "goal": Goal, "mode": Mode, diff --git a/worlds/alttp/Rom.py b/worlds/alttp/Rom.py index 05113514e484..f451e88fdb2b 100644 --- a/worlds/alttp/Rom.py +++ b/worlds/alttp/Rom.py @@ -2538,12 +2538,12 @@ def hint_text(dest, ped_hint=False): tt['menu_start_2'] = "{MENU}\n{SPEED0}\n≥@'s house\n Dark Chapel\n{CHOICE3}" tt['menu_start_3'] = "{MENU}\n{SPEED0}\n≥@'s house\n Dark Chapel\n Mountain Cave\n{CHOICE2}" - for at, text in world.plando_texts[player].items(): + for at, text, _ in world.plando_texts[player]: if at not in tt: raise Exception(f"No text target \"{at}\" found.") else: - tt[at] = text + tt[at] = "\n".join(text) rom.write_bytes(0xE0000, tt.getBytes()) diff --git a/worlds/alttp/Shops.py b/worlds/alttp/Shops.py index dbe8cc1f9dfa..db2b5b680c1d 100644 --- a/worlds/alttp/Shops.py +++ b/worlds/alttp/Shops.py @@ -9,9 +9,9 @@ from BaseClasses import CollectionState from .SubClasses import ALttPLocation -from .EntranceShuffle import door_addresses + from .Items import item_name_groups -from .Options import small_key_shuffle, RandomizeShopInventories + from .StateHelpers import has_hearts, can_use_bombs, can_hold_arrows logger = logging.getLogger("Shops") @@ -66,6 +66,7 @@ def item_count(self) -> int: return 0 def get_bytes(self) -> List[int]: + from .EntranceShuffle import door_addresses # [id][roomID-low][roomID-high][doorID][zero][shop_config][shopkeeper_config][sram_index] entrances = self.region.entrances config = self.item_count @@ -181,7 +182,7 @@ def push_shop_inventories(multiworld): def create_shops(multiworld, player: int): - + from .Options import RandomizeShopInventories player_shop_table = shop_table.copy() if multiworld.include_witch_hut[player]: player_shop_table["Potion Shop"] = player_shop_table["Potion Shop"]._replace(locked=False) @@ -304,6 +305,7 @@ class ShopData(NamedTuple): def set_up_shops(multiworld, player: int): + from .Options import small_key_shuffle # TODO: move hard+ mode changes for shields here, utilizing the new shops if multiworld.retro_bow[player]: @@ -426,7 +428,7 @@ def get_price_modifier(item): def get_price(multiworld, item, player: int, price_type=None): """Converts a raw Rupee price into a special price type""" - + from .Options import small_key_shuffle if price_type: price_types = [price_type] else: diff --git a/worlds/alttp/Text.py b/worlds/alttp/Text.py index b479a9b8e002..c005cacd8f9f 100644 --- a/worlds/alttp/Text.py +++ b/worlds/alttp/Text.py @@ -1289,6 +1289,415 @@ class LargeCreditBottomMapper(CharTextMapper): class TextTable(object): SIZE = 0x7355 + valid_keys = [ + "set_cursor", + "set_cursor2", + "game_over_menu", + "var_test", + "follower_no_enter", + "choice_1_3", + "choice_2_3", + "choice_3_3", + "choice_1_2", + "choice_2_2", + "uncle_leaving_text", + "uncle_dying_sewer", + "tutorial_guard_1", + "tutorial_guard_2", + "tutorial_guard_3", + "tutorial_guard_4", + "tutorial_guard_5", + "tutorial_guard_6", + "tutorial_guard_7", + "priest_sanctuary_before_leave", + "sanctuary_enter", + "zelda_sanctuary_story", + "priest_sanctuary_before_pendants", + "priest_sanctuary_after_pendants_before_master_sword", + "priest_sanctuary_dying", + "zelda_save_sewers", + "priest_info", + "zelda_sanctuary_before_leave", + "telepathic_intro", + "telepathic_reminder", + "zelda_go_to_throne", + "zelda_push_throne", + "zelda_switch_room_pull", + "zelda_save_lets_go", + "zelda_save_repeat", + "zelda_before_pendants", + "zelda_after_pendants_before_master_sword", + "telepathic_zelda_right_after_master_sword", + "zelda_sewers", + "zelda_switch_room", + "kakariko_saharalasa_wife", + "kakariko_saharalasa_wife_sword_story", + "kakariko_saharalasa_wife_closing", + "kakariko_saharalasa_after_master_sword", + "kakariko_alert_guards", + "sahasrahla_quest_have_pendants", + "sahasrahla_quest_have_master_sword", + "sahasrahla_quest_information", + "sahasrahla_bring_courage", + "sahasrahla_have_ice_rod", + "telepathic_sahasrahla_beat_agahnim", + "telepathic_sahasrahla_beat_agahnim_no_pearl", + "sahasrahla_have_boots_no_icerod", + "sahasrahla_have_courage", + "sahasrahla_found", + "sign_rain_north_of_links_house", + "sign_north_of_links_house", + "sign_path_to_death_mountain", + "sign_lost_woods", + "sign_zoras", + "sign_outside_magic_shop", + "sign_death_mountain_cave_back", + "sign_east_of_links_house", + "sign_south_of_lumberjacks", + "sign_east_of_desert", + "sign_east_of_sanctuary", + "sign_east_of_castle", + "sign_north_of_lake", + "sign_desert_thief", + "sign_lumberjacks_house", + "sign_north_kakariko", + "witch_bring_mushroom", + "witch_brewing_the_item", + "witch_assistant_no_bottle", + "witch_assistant_no_empty_bottle", + "witch_assistant_informational", + "witch_assistant_no_bottle_buying", + "potion_shop_no_empty_bottles", + "item_get_lamp", + "item_get_boomerang", + "item_get_bow", + "item_get_shovel", + "item_get_magic_cape", + "item_get_powder", + "item_get_flippers", + "item_get_power_gloves", + "item_get_pendant_courage", + "item_get_pendant_power", + "item_get_pendant_wisdom", + "item_get_mushroom", + "item_get_book", + "item_get_moonpearl", + "item_get_compass", + "item_get_map", + "item_get_ice_rod", + "item_get_fire_rod", + "item_get_ether", + "item_get_bombos", + "item_get_quake", + "item_get_hammer", + "item_get_flute", + "item_get_cane_of_somaria", + "item_get_hookshot", + "item_get_bombs", + "item_get_bottle", + "item_get_big_key", + "item_get_titans_mitts", + "item_get_magic_mirror", + "item_get_fake_mastersword", + "post_item_get_mastersword", + "item_get_red_potion", + "item_get_green_potion", + "item_get_blue_potion", + "item_get_bug_net", + "item_get_blue_mail", + "item_get_red_mail", + "item_get_temperedsword", + "item_get_mirror_shield", + "item_get_cane_of_byrna", + "missing_big_key", + "missing_magic", + "item_get_pegasus_boots", + "talking_tree_info_start", + "talking_tree_info_1", + "talking_tree_info_2", + "talking_tree_info_3", + "talking_tree_info_4", + "talking_tree_other", + "item_get_pendant_power_alt", + "item_get_pendant_wisdom_alt", + "game_shooting_choice", + "game_shooting_yes", + "game_shooting_no", + "game_shooting_continue", + "pond_of_wishing", + "pond_item_select", + "pond_item_test", + "pond_will_upgrade", + "pond_item_test_no", + "pond_item_test_no_no", + "pond_item_boomerang", + "pond_item_shield", + "pond_item_silvers", + "pond_item_bottle_filled", + "pond_item_sword", + "pond_of_wishing_happiness", + "pond_of_wishing_choice", + "pond_of_wishing_bombs", + "pond_of_wishing_arrows", + "pond_of_wishing_full_upgrades", + "mountain_old_man_first", + "mountain_old_man_deadend", + "mountain_old_man_turn_right", + "mountain_old_man_lost_and_alone", + "mountain_old_man_drop_off", + "mountain_old_man_in_his_cave_pre_agahnim", + "mountain_old_man_in_his_cave", + "mountain_old_man_in_his_cave_post_agahnim", + "tavern_old_man_awake", + "tavern_old_man_unactivated_flute", + "tavern_old_man_know_tree_unactivated_flute", + "tavern_old_man_have_flute", + "chicken_hut_lady", + "running_man", + "game_race_sign", + "sign_bumper_cave", + "sign_catfish", + "sign_north_village_of_outcasts", + "sign_south_of_bumper_cave", + "sign_east_of_pyramid", + "sign_east_of_bomb_shop", + "sign_east_of_mire", + "sign_village_of_outcasts", + "sign_before_wishing_pond", + "sign_before_catfish_area", + "castle_wall_guard", + "gate_guard", + "telepathic_tile_eastern_palace", + "telepathic_tile_tower_of_hera_floor_4", + "hylian_text_1", + "mastersword_pedestal_translated", + "telepathic_tile_spectacle_rock", + "telepathic_tile_swamp_entrance", + "telepathic_tile_thieves_town_upstairs", + "telepathic_tile_misery_mire", + "hylian_text_2", + "desert_entry_translated", + "telepathic_tile_under_ganon", + "telepathic_tile_palace_of_darkness", + "telepathic_tile_desert_bonk_torch_room", + "telepathic_tile_castle_tower", + "telepathic_tile_ice_large_room", + "telepathic_tile_turtle_rock", + "telepathic_tile_ice_entrance", + "telepathic_tile_ice_stalfos_knights_room", + "telepathic_tile_tower_of_hera_entrance", + "houlihan_room", + "caught_a_bee", + "caught_a_fairy", + "no_empty_bottles", + "game_race_boy_time", + "game_race_girl", + "game_race_boy_success", + "game_race_boy_failure", + "game_race_boy_already_won", + "game_race_boy_sneaky", + "bottle_vendor_choice", + "bottle_vendor_get", + "bottle_vendor_no", + "bottle_vendor_already_collected", + "bottle_vendor_bee", + "bottle_vendor_fish", + "hobo_item_get_bottle", + "blacksmiths_what_you_want", + "blacksmiths_paywall", + "blacksmiths_extra_okay", + "blacksmiths_tempered_already", + "blacksmiths_temper_no", + "blacksmiths_bogart_sword", + "blacksmiths_get_sword", + "blacksmiths_shop_before_saving", + "blacksmiths_shop_saving", + "blacksmiths_collect_frog", + "blacksmiths_still_working", + "blacksmiths_saving_bows", + "blacksmiths_hammer_anvil", + "dark_flute_boy_storytime", + "dark_flute_boy_get_shovel", + "dark_flute_boy_no_get_shovel", + "dark_flute_boy_flute_not_found", + "dark_flute_boy_after_shovel_get", + "shop_fortune_teller_lw_hint_0", + "shop_fortune_teller_lw_hint_1", + "shop_fortune_teller_lw_hint_2", + "shop_fortune_teller_lw_hint_3", + "shop_fortune_teller_lw_hint_4", + "shop_fortune_teller_lw_hint_5", + "shop_fortune_teller_lw_hint_6", + "shop_fortune_teller_lw_hint_7", + "shop_fortune_teller_lw_no_rupees", + "shop_fortune_teller_lw", + "shop_fortune_teller_lw_post_hint", + "shop_fortune_teller_lw_no", + "shop_fortune_teller_lw_hint_8", + "shop_fortune_teller_lw_hint_9", + "shop_fortune_teller_lw_hint_10", + "shop_fortune_teller_lw_hint_11", + "shop_fortune_teller_lw_hint_12", + "shop_fortune_teller_lw_hint_13", + "shop_fortune_teller_lw_hint_14", + "shop_fortune_teller_lw_hint_15", + "dark_sanctuary", + "dark_sanctuary_hint_0", + "dark_sanctuary_no", + "dark_sanctuary_hint_1", + "dark_sanctuary_yes", + "dark_sanctuary_hint_2", + "sick_kid_no_bottle", + "sick_kid_trade", + "sick_kid_post_trade", + "desert_thief_sitting", + "desert_thief_following", + "desert_thief_question", + "desert_thief_question_yes", + "desert_thief_after_item_get", + "desert_thief_reassure", + "hylian_text_3", + "tablet_ether_book", + "tablet_bombos_book", + "magic_bat_wake", + "magic_bat_give_half_magic", + "intro_main", + "intro_throne_room", + "intro_zelda_cell", + "intro_agahnim", + "pickup_purple_chest", + "bomb_shop", + "bomb_shop_big_bomb", + "bomb_shop_big_bomb_buy", + "item_get_big_bomb", + "kiki_second_extortion", + "kiki_second_extortion_no", + "kiki_second_extortion_yes", + "kiki_first_extortion", + "kiki_first_extortion_yes", + "kiki_first_extortion_no", + "kiki_leaving_screen", + "blind_in_the_cell", + "blind_by_the_light", + "blind_not_that_way", + "aginah_l1sword_no_book", + "aginah_l1sword_with_pendants", + "aginah", + "aginah_need_better_sword", + "aginah_have_better_sword", + "catfish", + "catfish_after_item", + "lumberjack_right", + "lumberjack_left", + "lumberjack_left_post_agahnim", + "fighting_brothers_right", + "fighting_brothers_right_opened", + "fighting_brothers_left", + "maiden_crystal_1", + "maiden_crystal_2", + "maiden_crystal_3", + "maiden_crystal_4", + "maiden_crystal_5", + "maiden_crystal_6", + "maiden_crystal_7", + "maiden_ending", + "maiden_confirm_understood", + "barrier_breaking", + "maiden_crystal_7_again", + "agahnim_zelda_teleport", + "agahnim_magic_running_away", + "agahnim_hide_and_seek_found", + "agahnim_defeated", + "agahnim_final_meeting", + "zora_meeting", + "zora_tells_cost", + "zora_get_flippers", + "zora_no_cash", + "zora_no_buy_item", + "kakariko_saharalasa_grandson", + "kakariko_saharalasa_grandson_next", + "dark_palace_tree_dude", + "fairy_wishing_ponds", + "fairy_wishing_ponds_no", + "pond_of_wishing_no", + "pond_of_wishing_return_item", + "pond_of_wishing_throw", + "pond_pre_item_silvers", + "pond_of_wishing_great_luck", + "pond_of_wishing_good_luck", + "pond_of_wishing_meh_luck", + "pond_of_wishing_bad_luck", + "pond_of_wishing_fortune", + "item_get_14_heart", + "item_get_24_heart", + "item_get_34_heart", + "item_get_whole_heart", + "item_get_sanc_heart", + "fairy_fountain_refill", + "death_mountain_bullied_no_pearl", + "death_mountain_bullied_with_pearl", + "death_mountain_bully_no_pearl", + "death_mountain_bully_with_pearl", + "shop_darkworld_enter", + "game_chest_village_of_outcasts", + "game_chest_no_cash", + "game_chest_not_played", + "game_chest_played", + "game_chest_village_of_outcasts_play", + "shop_first_time", + "shop_already_have", + "shop_buy_shield", + "shop_buy_red_potion", + "shop_buy_arrows", + "shop_buy_bombs", + "shop_buy_bee", + "shop_buy_heart", + "shop_first_no_bottle_buy", + "shop_buy_no_space", + "ganon_fall_in", + "ganon_phase_3", + "lost_woods_thief", + "blinds_hut_dude", + "end_triforce", + "toppi_fallen", + "kakariko_tavern_fisherman", + "thief_money", + "thief_desert_rupee_cave", + "thief_ice_rupee_cave", + "telepathic_tile_south_east_darkworld_cave", + "cukeman", + "cukeman_2", + "potion_shop_no_cash", + "kakariko_powdered_chicken", + "game_chest_south_of_kakariko", + "game_chest_play_yes", + "game_chest_play_no", + "game_chest_lost_woods", + "kakariko_flophouse_man_no_flippers", + "kakariko_flophouse_man", + "menu_start_2", + "menu_start_3", + "menu_pause", + "game_digging_choice", + "game_digging_start", + "game_digging_no_cash", + "game_digging_end_time", + "game_digging_come_back_later", + "game_digging_no_follower", + "menu_start_4", + "ganon_fall_in_alt", + "ganon_phase_3_alt", + "sign_east_death_mountain_bridge", + "fish_money", + "sign_ganons_tower", + "sign_ganon", + "ganon_phase_3_no_bow", + "ganon_phase_3_no_silvers_alt", + "ganon_phase_3_no_silvers", + "ganon_phase_3_silvers", + "murahdahla", + ] + def __init__(self): self._text = OrderedDict() self.setDefaultText() diff --git a/worlds/generic/__init__.py b/worlds/generic/__init__.py index b88295b43237..29f808b20272 100644 --- a/worlds/generic/__init__.py +++ b/worlds/generic/__init__.py @@ -68,9 +68,3 @@ def failed(self, warning: str, exception=Exception): raise exception(warning) else: self.warn(warning) - - -class PlandoConnection(NamedTuple): - entrance: str - exit: str - direction: str # entrance, exit or both diff --git a/worlds/kdl3/Options.py b/worlds/kdl3/Options.py index 336bd33bc583..e0a4f12f15dc 100644 --- a/worlds/kdl3/Options.py +++ b/worlds/kdl3/Options.py @@ -2,10 +2,14 @@ from dataclasses import dataclass from Options import DeathLink, Choice, Toggle, OptionDict, Range, PlandoBosses, DefaultOnToggle, \ - PerGameCommonOptions + PerGameCommonOptions, PlandoConnections from .Names import LocationName +class KDL3PlandoConnections(PlandoConnections): + entrances = exits = {f"{i} {j}" for i in LocationName.level_names for j in range(1, 7)} + + class Goal(Choice): """ Zero: collect the Heart Stars, and defeat Zero in the Hyper Zone. @@ -400,6 +404,7 @@ class Gifting(Toggle): @dataclass class KDL3Options(PerGameCommonOptions): + plando_connections: KDL3PlandoConnections death_link: DeathLink game_language: GameLanguage goal: Goal diff --git a/worlds/kdl3/Regions.py b/worlds/kdl3/Regions.py index ac27d8bbf517..407dcf9680f4 100644 --- a/worlds/kdl3/Regions.py +++ b/worlds/kdl3/Regions.py @@ -129,8 +129,8 @@ def generate_valid_levels(world: "KDL3World", enforce_world: bool, enforce_patte } possible_stages = [default_levels[level][stage] for level in default_levels for stage in range(6)] - if world.multiworld.plando_connections[world.player]: - for connection in world.multiworld.plando_connections[world.player]: + if world.options.plando_connections: + for connection in world.options.plando_connections: try: entrance_world, entrance_stage = connection.entrance.rsplit(" ", 1) stage_world, stage_stage = connection.exit.rsplit(" ", 1) diff --git a/worlds/kdl3/test/__init__.py b/worlds/kdl3/test/__init__.py index 11a17e63b7fa..4d3f4d70faae 100644 --- a/worlds/kdl3/test/__init__.py +++ b/worlds/kdl3/test/__init__.py @@ -2,7 +2,7 @@ from argparse import Namespace from BaseClasses import MultiWorld, PlandoOptions, CollectionState -from test.TestBase import WorldTestBase +from test.bases import WorldTestBase from test.general import gen_steps from worlds import AutoWorld from worlds.AutoWorld import call_all @@ -32,6 +32,5 @@ def world_setup(self, seed: typing.Optional[int] = None) -> None: }) self.multiworld.set_options(args) self.multiworld.plando_options = PlandoOptions.connections - self.multiworld.plando_connections = self.options["plando_connections"] if "plando_connections" in self.options.keys() else [] for step in gen_steps: call_all(self.multiworld, step) diff --git a/worlds/kdl3/test/test_locations.py b/worlds/kdl3/test/test_locations.py index 433b4534d1e5..bde9abc409ac 100644 --- a/worlds/kdl3/test/test_locations.py +++ b/worlds/kdl3/test/test_locations.py @@ -1,5 +1,5 @@ from . import KDL3TestBase -from worlds.generic import PlandoConnection +from Options import PlandoConnection from ..Names import LocationName import typing @@ -49,12 +49,10 @@ class TestShiro(KDL3TestBase): options = { "open_world": False, "plando_connections": [ - [], - [ PlandoConnection("Grass Land 1", "Iceberg 5", "both"), PlandoConnection("Grass Land 2", "Ripple Field 5", "both"), PlandoConnection("Grass Land 3", "Grass Land 1", "both") - ]], + ], "stage_shuffle": "shuffled", "plando_options": "connections" } diff --git a/worlds/messenger/options.py b/worlds/messenger/options.py index 0d8fcf4da55f..73adf4ebdf0a 100644 --- a/worlds/messenger/options.py +++ b/worlds/messenger/options.py @@ -3,8 +3,9 @@ from schema import And, Optional, Or, Schema -from Options import Accessibility, Choice, DeathLinkMixin, DefaultOnToggle, OptionDict, PerGameCommonOptions, Range, \ - StartInventoryPool, Toggle +from Options import Accessibility, Choice, DeathLinkMixin, DefaultOnToggle, OptionDict, PerGameCommonOptions, \ + PlandoConnections, Range, StartInventoryPool, Toggle, Visibility +from worlds.messenger.portals import CHECKPOINTS, PORTALS, SHOP_POINTS class MessengerAccessibility(Accessibility): @@ -13,6 +14,36 @@ class MessengerAccessibility(Accessibility): __doc__ = Accessibility.__doc__.replace(f"default {Accessibility.default}", f"default {default}") +class PortalPlando(PlandoConnections): + """ + Plando connections to be used with portal shuffle. Direction is ignored. + List of valid connections can be found here: https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/messenger/portals.py#L12. + The entering Portal should *not* have "Portal" appended. + For the exits, those in checkpoints and shops should just be the name of the spot, while portals should have " Portal" at the end. + Example: + - entrance: Riviere Turquoise + exit: Wingsuit + - entrance: Sunken Shrine + exit: Sunny Day + - entrance: Searing Crags + exit: Glacial Peak Portal + """ + portals = [f"{portal} Portal" for portal in PORTALS] + shop_points = [point for points in SHOP_POINTS.values() for point in points] + checkpoints = [point for points in CHECKPOINTS.values() for point in points] + portal_entrances = PORTALS + portal_exits = portals + shop_points + checkpoints + entrances = portal_entrances + exits = portal_exits + + +# for back compatibility. To later be replaced with transition plando +class HiddenPortalPlando(PortalPlando): + visibility = Visibility.none + entrances = PortalPlando.entrances + exits = PortalPlando.exits + + class Logic(Choice): """ The level of logic to use when determining what locations in your world are accessible. @@ -205,3 +236,5 @@ class MessengerOptions(DeathLinkMixin, PerGameCommonOptions): traps: Traps shop_price: ShopPrices shop_price_plan: PlannedShopPrices + portal_plando: PortalPlando + plando_connections: HiddenPortalPlando diff --git a/worlds/messenger/portals.py b/worlds/messenger/portals.py index f5603736c3a7..1da210cb23ff 100644 --- a/worlds/messenger/portals.py +++ b/worlds/messenger/portals.py @@ -2,8 +2,7 @@ from typing import List, TYPE_CHECKING from BaseClasses import CollectionState, PlandoOptions -from worlds.generic import PlandoConnection -from .options import ShufflePortals +from Options import PlandoConnection if TYPE_CHECKING: from . import MessengerWorld @@ -207,6 +206,8 @@ def shuffle_portals(world: "MessengerWorld") -> None: """shuffles the output of the portals from the main hub""" + from .options import ShufflePortals + def create_mapping(in_portal: str, warp: str) -> str: """assigns the chosen output to the input""" parent = out_to_parent[warp] @@ -247,7 +248,9 @@ def handle_planned_portals(plando_connections: List[PlandoConnection]) -> None: available_portals = [val for zone in shop_points.values() for val in zone] world.random.shuffle(available_portals) - plando = world.multiworld.plando_connections[world.player] + plando = world.options.portal_plando.value + if not plando: + plando = world.options.plando_connections.value if plando and world.multiworld.plando_options & PlandoOptions.connections: handle_planned_portals(plando) diff --git a/worlds/minecraft/Options.py b/worlds/minecraft/Options.py index cdb5bf303f47..9407097b4638 100644 --- a/worlds/minecraft/Options.py +++ b/worlds/minecraft/Options.py @@ -1,5 +1,6 @@ import typing -from Options import Choice, Option, Toggle, DefaultOnToggle, Range, OptionList, DeathLink +from Options import Choice, Option, Toggle, DefaultOnToggle, Range, OptionList, DeathLink, PlandoConnections +from .Constants import region_info class AdvancementGoal(Range): @@ -97,7 +98,19 @@ class StartingItems(OptionList): display_name = "Starting Items" +class MCPlandoConnections(PlandoConnections): + entrances = set(connection[0] for connection in region_info["default_connections"]) + exits = set(connection[1] for connection in region_info["default_connections"]) + + @classmethod + def can_connect(cls, entrance, exit): + if exit in region_info["illegal_connections"] and entrance in region_info["illegal_connections"][exit]: + return False + return True + + minecraft_options: typing.Dict[str, type(Option)] = { + "plando_connections": MCPlandoConnections, "advancement_goal": AdvancementGoal, "egg_shards_required": EggShardsRequired, "egg_shards_available": EggShardsAvailable, diff --git a/worlds/oot/Options.py b/worlds/oot/Options.py index 2543cdc715c7..daf072adb59c 100644 --- a/worlds/oot/Options.py +++ b/worlds/oot/Options.py @@ -1,6 +1,7 @@ import typing import random -from Options import Option, DefaultOnToggle, Toggle, Range, OptionList, OptionSet, DeathLink +from Options import Option, DefaultOnToggle, Toggle, Range, OptionList, OptionSet, DeathLink, PlandoConnections +from .EntranceShuffle import entrance_shuffle_table from .LogicTricks import normalized_name_tricks from .ColorSFXOptions import * @@ -29,6 +30,11 @@ def from_any(cls, data: typing.Any) -> Range: raise RuntimeError(f"All options specified in \"{cls.display_name}\" are weighted as zero.") +class OoTPlandoConnections(PlandoConnections): + entrances = set([connection[1][0] for connection in entrance_shuffle_table]) + exits = set([connection[2][0] for connection in entrance_shuffle_table if len(connection) > 2]) + + class Logic(Choice): """Set the logic used for the generator. Glitchless: Normal gameplay. Can enable more difficult logical paths using the Logic Tricks option. @@ -1277,6 +1283,7 @@ class LogicTricks(OptionList): # All options assembled into a single dict oot_options: typing.Dict[str, type(Option)] = { + "plando_connections": OoTPlandoConnections, "logic_rules": Logic, "logic_no_night_tokens_without_suns_song": NightTokens, **open_options, diff --git a/worlds/oot/__init__.py b/worlds/oot/__init__.py index 9346ac55103a..34b3935fec4e 100644 --- a/worlds/oot/__init__.py +++ b/worlds/oot/__init__.py @@ -32,7 +32,7 @@ from Utils import get_options from BaseClasses import MultiWorld, CollectionState, Tutorial, LocationProgressType -from Options import Range, Toggle, VerifyKeys, Accessibility +from Options import Range, Toggle, VerifyKeys, Accessibility, PlandoConnections from Fill import fill_restrictive, fast_fill, FillError from worlds.generic.Rules import exclusion_rules, add_item_rule from ..AutoWorld import World, AutoLogicRegister, WebWorld @@ -201,6 +201,8 @@ def generate_early(self): option_value = bool(result) elif isinstance(result, VerifyKeys): option_value = result.value + elif isinstance(result, PlandoConnections): + option_value = result.value else: option_value = result.current_key setattr(self, option_name, option_value) diff --git a/worlds/tunic/__init__.py b/worlds/tunic/__init__.py index cff8c39c9fea..9ef5800955aa 100644 --- a/worlds/tunic/__init__.py +++ b/worlds/tunic/__init__.py @@ -10,7 +10,7 @@ from .er_data import portal_mapping from .options import TunicOptions, EntranceRando, tunic_option_groups, tunic_option_presets from worlds.AutoWorld import WebWorld, World -from worlds.generic import PlandoConnection +from Options import PlandoConnection from decimal import Decimal, ROUND_HALF_UP @@ -70,17 +70,17 @@ class TunicWorld(World): seed_groups: Dict[str, SeedGroup] = {} def generate_early(self) -> None: - if self.multiworld.plando_connections[self.player]: - for index, cxn in enumerate(self.multiworld.plando_connections[self.player]): + if self.options.plando_connections: + for index, cxn in enumerate(self.options.plando_connections): # making shops second to simplify other things later if cxn.entrance.startswith("Shop"): replacement = PlandoConnection(cxn.exit, "Shop Portal", "both") - self.multiworld.plando_connections[self.player].remove(cxn) - self.multiworld.plando_connections[self.player].insert(index, replacement) + self.options.plando_connections.value.remove(cxn) + self.options.plando_connections.value.insert(index, replacement) elif cxn.exit.startswith("Shop"): replacement = PlandoConnection(cxn.entrance, "Shop Portal", "both") - self.multiworld.plando_connections[self.player].remove(cxn) - self.multiworld.plando_connections[self.player].insert(index, replacement) + self.options.plando_connections.value.remove(cxn) + self.options.plando_connections.value.insert(index, replacement) # Universal tracker stuff, shouldn't do anything in standard gen if hasattr(self.multiworld, "re_gen_passthrough"): diff --git a/worlds/tunic/er_scripts.py b/worlds/tunic/er_scripts.py index 3abdfecce233..7e022c9f3a0d 100644 --- a/worlds/tunic/er_scripts.py +++ b/worlds/tunic/er_scripts.py @@ -3,8 +3,8 @@ from .locations import location_table from .er_data import Portal, tunic_er_regions, portal_mapping, traversal_requirements, DeadEnd from .er_rules import set_er_region_rules +from Options import PlandoConnection from .options import EntranceRando -from worlds.generic import PlandoConnection from random import Random from copy import deepcopy @@ -194,7 +194,7 @@ def pair_portals(world: "TunicWorld") -> Dict[Portal, Portal]: connected_regions = update_reachable_regions(connected_regions, traversal_reqs, has_laurels, logic_rules) if world.options.entrance_rando.value in EntranceRando.options: - plando_connections = world.multiworld.plando_connections[world.player] + plando_connections = world.options.plando_connections.value else: plando_connections = world.seed_groups[world.options.entrance_rando.value]["plando"] diff --git a/worlds/tunic/options.py b/worlds/tunic/options.py index a45ee71b0557..b3b6b3b96fb0 100644 --- a/worlds/tunic/options.py +++ b/worlds/tunic/options.py @@ -1,7 +1,8 @@ from dataclasses import dataclass from typing import Dict, Any -from Options import (DefaultOnToggle, Toggle, StartInventoryPool, Choice, Range, TextChoice, PerGameCommonOptions, - OptionGroup) +from Options import (DefaultOnToggle, Toggle, StartInventoryPool, Choice, Range, TextChoice, PlandoConnections, + PerGameCommonOptions, OptionGroup) +from .er_data import portal_mapping class SwordProgression(DefaultOnToggle): @@ -170,6 +171,13 @@ class ShuffleLadders(Toggle): """ internal_name = "shuffle_ladders" display_name = "Shuffle Ladders" + + +class TUNICPlandoConnections(PlandoConnections): + entrances = {*(portal.name for portal in portal_mapping), "Shop", "Shop Portal"} + exits = {*(portal.name for portal in portal_mapping), "Shop", "Shop Portal"} + + duplicate_exits = True @dataclass @@ -190,7 +198,8 @@ class TunicOptions(PerGameCommonOptions): lanternless: Lanternless maskless: Maskless laurels_location: LaurelsLocation - + plando_connections: TUNICPlandoConnections + tunic_option_groups = [ OptionGroup("Logic Options", [ From 97c9c5310bb89a04c862de7689925802397942b0 Mon Sep 17 00:00:00 2001 From: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> Date: Sat, 1 Jun 2024 07:35:33 -0400 Subject: [PATCH 21/27] PKMN R/B: Fixing Key Items Only + Removed Exp. All (#3420) --- worlds/pokemon_rb/regions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worlds/pokemon_rb/regions.py b/worlds/pokemon_rb/regions.py index 4932f5793583..a9206fe66753 100644 --- a/worlds/pokemon_rb/regions.py +++ b/worlds/pokemon_rb/regions.py @@ -1560,7 +1560,7 @@ def create_regions(self): <= self.multiworld.trap_percentage[self.player].value and combined_traps != 0): item = self.create_item(self.select_trap()) - if self.multiworld.key_items_only[self.player] and (not location.event) and (not item.advancement): + if self.multiworld.key_items_only[self.player] and (not location.event) and (not item.advancement) and location.original_item != "Exp. All": continue if item.name in start_inventory and start_inventory[item.name] > 0 and \ From 1e205f9d738fbcb552c29722c879afd1b9f757f2 Mon Sep 17 00:00:00 2001 From: Dinopony Date: Sat, 1 Jun 2024 13:39:57 +0200 Subject: [PATCH 22/27] Landstalker: Fixed rare generation issues (#3353) Co-authored-by: Fabian Dill --- worlds/landstalker/Hints.py | 5 ++++- worlds/landstalker/Locations.py | 15 ++++++++++++++- worlds/landstalker/Regions.py | 2 +- worlds/landstalker/Rules.py | 2 +- worlds/landstalker/__init__.py | 3 +++ 5 files changed, 23 insertions(+), 4 deletions(-) diff --git a/worlds/landstalker/Hints.py b/worlds/landstalker/Hints.py index 93274f1d68bb..5309e85032ea 100644 --- a/worlds/landstalker/Hints.py +++ b/worlds/landstalker/Hints.py @@ -30,6 +30,9 @@ def generate_lithograph_hint(world: "LandstalkerWorld"): jewel_items = world.jewel_items for item in jewel_items: + if item.location is None: + continue + # Jewel hints are composed of 4 'words' shuffled randomly: # - the name of the player whose world contains said jewel (if not ours) # - the color of the jewel (if relevant) @@ -61,7 +64,7 @@ def generate_random_hints(world: "LandstalkerWorld"): excluded_items = ["Life Stock", "EkeEke"] progression_items = [item for item in multiworld.itempool if item.advancement and - item.name not in excluded_items] + item.name not in excluded_items and item.location is not None] local_own_progression_items = [item for item in progression_items if item.player == this_player and item.location.player == this_player] diff --git a/worlds/landstalker/Locations.py b/worlds/landstalker/Locations.py index 5e42fbecda72..b0148269eab3 100644 --- a/worlds/landstalker/Locations.py +++ b/worlds/landstalker/Locations.py @@ -1,8 +1,9 @@ from typing import Dict, Optional -from BaseClasses import Location +from BaseClasses import Location, ItemClassification, Item from .Regions import LandstalkerRegion from .data.item_source import ITEM_SOURCES_JSON +from .data.world_path import WORLD_PATHS_JSON BASE_LOCATION_ID = 4000 BASE_GROUND_LOCATION_ID = BASE_LOCATION_ID + 256 @@ -28,6 +29,18 @@ def create_locations(player: int, regions_table: Dict[str, LandstalkerRegion], n new_location = LandstalkerLocation(player, data["name"], name_to_id_table[data["name"]], region, data["type"]) region.locations.append(new_location) + # Create fake event locations that will be used to determine if some key regions has been visited + regions_with_entrance_checks = [] + for data in WORLD_PATHS_JSON: + if "requiredNodes" in data: + regions_with_entrance_checks.extend([region_id for region_id in data["requiredNodes"]]) + regions_with_entrance_checks = list(set(regions_with_entrance_checks)) + for region_id in regions_with_entrance_checks: + region = regions_table[region_id] + location = LandstalkerLocation(player, 'event_visited_' + region_id, None, region, "event") + location.place_locked_item(Item("event_visited_" + region_id, ItemClassification.progression, None, player)) + region.locations.append(location) + # Create a specific end location that will contain a fake win-condition item end_location = LandstalkerLocation(player, "End", None, regions_table["end"], "reward") regions_table["end"].locations.append(end_location) diff --git a/worlds/landstalker/Regions.py b/worlds/landstalker/Regions.py index 21704194f157..27e5e2e993d0 100644 --- a/worlds/landstalker/Regions.py +++ b/worlds/landstalker/Regions.py @@ -37,7 +37,7 @@ def create_regions(world: "LandstalkerWorld"): for code, region_data in WORLD_NODES_JSON.items(): random_hint_name = None if "hints" in region_data: - random_hint_name = multiworld.random.choice(region_data["hints"]) + random_hint_name = world.random.choice(region_data["hints"]) region = LandstalkerRegion(code, region_data["name"], player, multiworld, random_hint_name) regions_table[code] = region multiworld.regions.append(region) diff --git a/worlds/landstalker/Rules.py b/worlds/landstalker/Rules.py index 51357c9480b0..94171944d7b2 100644 --- a/worlds/landstalker/Rules.py +++ b/worlds/landstalker/Rules.py @@ -10,7 +10,7 @@ def _landstalker_has_visited_regions(state: CollectionState, player: int, regions): - return all([state.can_reach(region, None, player) for region in regions]) + return all(state.has("event_visited_" + region.code, player) for region in regions) def _landstalker_has_health(state: CollectionState, player: int, health): diff --git a/worlds/landstalker/__init__.py b/worlds/landstalker/__init__.py index baa1deb620a4..2b3dc41239c3 100644 --- a/worlds/landstalker/__init__.py +++ b/worlds/landstalker/__init__.py @@ -204,6 +204,9 @@ def set_rules(self): for location in self.multiworld.get_locations(self.player): if location.parent_region.name in excluded_regions: location.progress_type = LocationProgressType.EXCLUDED + # We need to make that event non-progression since it would crash generation in reach_kazalt goal + if location.item is not None and location.item.name == "event_visited_king_nole_labyrinth_raft_entrance": + location.item.classification = ItemClassification.filler def get_starting_health(self): spawn_id = self.options.spawn_region.current_key From 8dbc8d2d41c743b9425809065f90f2d7935fe626 Mon Sep 17 00:00:00 2001 From: Nicholas Saylor <79181893+nicholassaylor@users.noreply.github.com> Date: Sat, 1 Jun 2024 07:42:02 -0400 Subject: [PATCH 23/27] Installer: Prevent ALTTP Sprite Download from being Interrupted (#3293) --- inno_setup.iss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/inno_setup.iss b/inno_setup.iss index 4b37279e8de0..b016f224dfcf 100644 --- a/inno_setup.iss +++ b/inno_setup.iss @@ -75,7 +75,7 @@ Name: "{commondesktop}\{#MyAppName} Launcher"; Filename: "{app}\ArchipelagoLaunc [Run] Filename: "{tmp}\vc_redist.x64.exe"; Parameters: "/passive /norestart"; Check: IsVCRedist64BitNeeded; StatusMsg: "Installing VC++ redistributable..." -Filename: "{app}\ArchipelagoLttPAdjuster"; Parameters: "--update_sprites"; StatusMsg: "Updating Sprite Library..."; Flags: nowait; Components: lttp_sprites +Filename: "{app}\ArchipelagoLttPAdjuster"; Parameters: "--update_sprites"; StatusMsg: "Updating Sprite Library..."; Components: lttp_sprites Filename: "{app}\ArchipelagoLauncher"; Parameters: "--update_settings"; StatusMsg: "Updating host.yaml..."; Flags: runasoriginaluser runhidden Filename: "{app}\ArchipelagoLauncher"; Description: "{cm:LaunchProgram,{#StringChange('Launcher', '&', '&&')}}"; Flags: nowait postinstall skipifsilent From 3cb545245528e46e9c0b72ade32839865efb3780 Mon Sep 17 00:00:00 2001 From: Ishigh1 Date: Sat, 1 Jun 2024 14:32:41 +0200 Subject: [PATCH 24/27] Core: Fix auto-fill in the text client when clicking on a hint suggestion (#3267) --- MultiServer.py | 24 +------------ Utils.py | 35 +++++++++++++++++++ kvui.py | 16 +++------ .../general/test_client_server_interaction.py | 23 ++++++++++++ 4 files changed, 64 insertions(+), 34 deletions(-) create mode 100644 test/general/test_client_server_interaction.py diff --git a/MultiServer.py b/MultiServer.py index 2c08b0b4eb08..22375da2b3c5 100644 --- a/MultiServer.py +++ b/MultiServer.py @@ -37,7 +37,7 @@ import NetUtils import Utils -from Utils import version_tuple, restricted_loads, Version, async_start +from Utils import version_tuple, restricted_loads, Version, async_start, get_intended_text from NetUtils import Endpoint, ClientStatus, NetworkItem, decode, encode, NetworkPlayer, Permission, NetworkSlot, \ SlotType, LocationStore @@ -1095,28 +1095,6 @@ def json_format_send_event(net_item: NetworkItem, receiving_player: int): "item": net_item} -def get_intended_text(input_text: str, possible_answers) -> typing.Tuple[str, bool, str]: - picks = Utils.get_fuzzy_results(input_text, possible_answers, limit=2) - if len(picks) > 1: - dif = picks[0][1] - picks[1][1] - if picks[0][1] == 100: - return picks[0][0], True, "Perfect Match" - elif picks[0][1] < 75: - return picks[0][0], False, f"Didn't find something that closely matches '{input_text}', " \ - f"did you mean '{picks[0][0]}'? ({picks[0][1]}% sure)" - elif dif > 5: - return picks[0][0], True, "Close Match" - else: - return picks[0][0], False, f"Too many close matches for '{input_text}', " \ - f"did you mean '{picks[0][0]}'? ({picks[0][1]}% sure)" - else: - if picks[0][1] > 90: - return picks[0][0], True, "Only Option Match" - else: - return picks[0][0], False, f"Didn't find something that closely matches '{input_text}', " \ - f"did you mean '{picks[0][0]}'? ({picks[0][1]}% sure)" - - class CommandMeta(type): def __new__(cls, name, bases, attrs): commands = attrs["commands"] = {} diff --git a/Utils.py b/Utils.py index 9f6837215c5e..eea81a2d3201 100644 --- a/Utils.py +++ b/Utils.py @@ -622,6 +622,41 @@ def get_fuzzy_ratio(word1: str, word2: str) -> float: ) +def get_intended_text(input_text: str, possible_answers) -> typing.Tuple[str, bool, str]: + picks = get_fuzzy_results(input_text, possible_answers, limit=2) + if len(picks) > 1: + dif = picks[0][1] - picks[1][1] + if picks[0][1] == 100: + return picks[0][0], True, "Perfect Match" + elif picks[0][1] < 75: + return picks[0][0], False, f"Didn't find something that closely matches '{input_text}', " \ + f"did you mean '{picks[0][0]}'? ({picks[0][1]}% sure)" + elif dif > 5: + return picks[0][0], True, "Close Match" + else: + return picks[0][0], False, f"Too many close matches for '{input_text}', " \ + f"did you mean '{picks[0][0]}'? ({picks[0][1]}% sure)" + else: + if picks[0][1] > 90: + return picks[0][0], True, "Only Option Match" + else: + return picks[0][0], False, f"Didn't find something that closely matches '{input_text}', " \ + f"did you mean '{picks[0][0]}'? ({picks[0][1]}% sure)" + + +def get_input_text_from_response(text: str, command: str) -> typing.Optional[str]: + if "did you mean " in text: + for question in ("Didn't find something that closely matches", + "Too many close matches"): + if text.startswith(question): + name = get_text_between(text, "did you mean '", + "'? (") + return f"!{command} {name}" + elif text.startswith("Missing: "): + return text.replace("Missing: ", "!hint_location ") + return None + + def open_filename(title: str, filetypes: typing.Iterable[typing.Tuple[str, typing.Iterable[str]]], suggest: str = "") \ -> typing.Optional[str]: logging.info(f"Opening file input dialog for {title}.") diff --git a/kvui.py b/kvui.py index 98aa9516b266..e9e495aef366 100644 --- a/kvui.py +++ b/kvui.py @@ -64,7 +64,7 @@ fade_in_animation = Animation(opacity=0, duration=0) + Animation(opacity=1, duration=0.25) from NetUtils import JSONtoTextParser, JSONMessagePart, SlotType -from Utils import async_start +from Utils import async_start, get_input_text_from_response if typing.TYPE_CHECKING: import CommonClient @@ -285,16 +285,10 @@ def on_touch_down(self, touch): temp = MarkupLabel(text=self.text).markup text = "".join(part for part in temp if not part.startswith(("[color", "[/color]", "[ref=", "[/ref]"))) cmdinput = App.get_running_app().textinput - if not cmdinput.text and " did you mean " in text: - for question in ("Didn't find something that closely matches, did you mean ", - "Too many close matches, did you mean "): - if text.startswith(question): - name = Utils.get_text_between(text, question, - "? (") - cmdinput.text = f"!{App.get_running_app().last_autofillable_command} {name}" - break - elif not cmdinput.text and text.startswith("Missing: "): - cmdinput.text = text.replace("Missing: ", "!hint_location ") + if not cmdinput.text: + input_text = get_input_text_from_response(text, App.get_running_app().last_autofillable_command) + if input_text is not None: + cmdinput.text = input_text Clipboard.copy(text.replace("&", "&").replace("&bl;", "[").replace("&br;", "]")) return self.parent.select_with_touch(self.index, touch) diff --git a/test/general/test_client_server_interaction.py b/test/general/test_client_server_interaction.py new file mode 100644 index 000000000000..17de91517409 --- /dev/null +++ b/test/general/test_client_server_interaction.py @@ -0,0 +1,23 @@ +import unittest + +from Utils import get_intended_text, get_input_text_from_response + + +class TestClient(unittest.TestCase): + def test_autofill_hint_from_fuzzy_hint(self) -> None: + tests = ( + ("item", ["item1", "item2"]), # Multiple close matches + ("itm", ["item1", "item21"]), # No close match, multiple option + ("item", ["item1"]), # No close match, single option + ("item", ["\"item\" 'item' (item)"]), # Testing different special characters + ) + + for input_text, possible_answers in tests: + item_name, usable, response = get_intended_text(input_text, possible_answers) + self.assertFalse(usable, "This test must be updated, it seems get_fuzzy_results behavior changed") + + hint_command = get_input_text_from_response(response, "hint") + self.assertIsNotNone(hint_command, + "The response to fuzzy hints is no longer recognized by the hint autofill") + self.assertEqual(hint_command, f"!hint {item_name}", + "The hint command autofilled by the response is not correct") From bbc79a5b998f1450fba90b0290745f88a7ffefc5 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Sat, 1 Jun 2024 14:38:45 +0200 Subject: [PATCH 25/27] LttP: allow Triforce Piece as start inventory item (#3292) --- worlds/alttp/Rom.py | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/worlds/alttp/Rom.py b/worlds/alttp/Rom.py index f451e88fdb2b..224de6aaf7f3 100644 --- a/worlds/alttp/Rom.py +++ b/worlds/alttp/Rom.py @@ -1269,7 +1269,8 @@ def chunk(l, n): rom.write_int32(0x18020C, 0) # starting time (in frames, sint32) # set up goals for treasure hunt - rom.write_int16(0x180163, local_world.treasure_hunt_required) + rom.write_int16(0x180163, max(0, local_world.treasure_hunt_required - + sum(1 for item in world.precollected_items[player] if item.name == "Triforce Piece"))) rom.write_bytes(0x180165, [0x0E, 0x28]) # Triforce Piece Sprite rom.write_byte(0x180194, 1) # Must turn in triforced pieces (instant win not enabled) @@ -1372,7 +1373,7 @@ def chunk(l, n): 'Golden Sword', 'Tempered Sword', 'Master Sword', 'Fighter Sword', 'Progressive Sword', 'Mirror Shield', 'Red Shield', 'Blue Shield', 'Progressive Shield', 'Red Mail', 'Blue Mail', 'Progressive Mail', - 'Magic Upgrade (1/4)', 'Magic Upgrade (1/2)'}: + 'Magic Upgrade (1/4)', 'Magic Upgrade (1/2)', 'Triforce Piece'}: continue set_table = {'Book of Mudora': (0x34E, 1), 'Hammer': (0x34B, 1), 'Bug Catching Net': (0x34D, 1), @@ -2475,6 +2476,9 @@ def hint_text(dest, ped_hint=False): tt['sahasrahla_quest_have_master_sword'] = Sahasrahla2_texts[local_random.randint(0, len(Sahasrahla2_texts) - 1)] tt['blind_by_the_light'] = Blind_texts[local_random.randint(0, len(Blind_texts) - 1)] + triforce_pieces_required = max(0, w.treasure_hunt_required - + sum(1 for item in world.precollected_items[player] if item.name == "Triforce Piece")) + if world.goal[player] in ['triforce_hunt', 'local_triforce_hunt']: tt['ganon_fall_in_alt'] = 'Why are you even here?\n You can\'t even hurt me! Get the Triforce Pieces.' tt['ganon_phase_3_alt'] = 'Seriously? Go Away, I will not Die.' @@ -2482,16 +2486,16 @@ def hint_text(dest, ped_hint=False): tt['sign_ganon'] = 'Go find the Triforce pieces with your friends... Ganon is invincible!' else: tt['sign_ganon'] = 'Go find the Triforce pieces... Ganon is invincible!' - if w.treasure_hunt_required > 1: + if triforce_pieces_required > 1: tt['murahdahla'] = "Hello @. I\nam Murahdahla, brother of\nSahasrahla and Aginah. Behold the power of\n" \ "invisibility.\n\n\n\n… … …\n\nWait! you can see me? I knew I should have\n" \ "hidden in a hollow tree. If you bring\n%d Triforce pieces out of %d, I can reassemble it." % \ - (w.treasure_hunt_required, w.treasure_hunt_total) + (triforce_pieces_required, w.treasure_hunt_total) else: tt['murahdahla'] = "Hello @. I\nam Murahdahla, brother of\nSahasrahla and Aginah. Behold the power of\n" \ "invisibility.\n\n\n\n… … …\n\nWait! you can see me? I knew I should have\n" \ "hidden in a hollow tree. If you bring\n%d Triforce piece out of %d, I can reassemble it." % \ - (w.treasure_hunt_required, w.treasure_hunt_total) + (triforce_pieces_required, w.treasure_hunt_total) elif world.goal[player] in ['pedestal']: tt['ganon_fall_in_alt'] = 'Why are you even here?\n You can\'t even hurt me! Your goal is at the pedestal.' tt['ganon_phase_3_alt'] = 'Seriously? Go Away, I will not Die.' @@ -2500,20 +2504,20 @@ def hint_text(dest, ped_hint=False): tt['ganon_fall_in'] = Ganon1_texts[local_random.randint(0, len(Ganon1_texts) - 1)] tt['ganon_fall_in_alt'] = 'You cannot defeat me until you finish your goal!' tt['ganon_phase_3_alt'] = 'Got wax in\nyour ears?\nI can not die!' - if w.treasure_hunt_required > 1: + if triforce_pieces_required > 1: if world.goal[player] == 'ganon_triforce_hunt' and world.players > 1: tt['sign_ganon'] = 'You need to find %d Triforce pieces out of %d with your friends to defeat Ganon.' % \ - (w.treasure_hunt_required, w.treasure_hunt_total) + (triforce_pieces_required, w.treasure_hunt_total) elif world.goal[player] in ['ganon_triforce_hunt', 'local_ganon_triforce_hunt']: tt['sign_ganon'] = 'You need to find %d Triforce pieces out of %d to defeat Ganon.' % \ - (w.treasure_hunt_required, w.treasure_hunt_total) + (triforce_pieces_required, w.treasure_hunt_total) else: if world.goal[player] == 'ganon_triforce_hunt' and world.players > 1: tt['sign_ganon'] = 'You need to find %d Triforce piece out of %d with your friends to defeat Ganon.' % \ - (w.treasure_hunt_required, w.treasure_hunt_total) + (triforce_pieces_required, w.treasure_hunt_total) elif world.goal[player] in ['ganon_triforce_hunt', 'local_ganon_triforce_hunt']: tt['sign_ganon'] = 'You need to find %d Triforce piece out of %d to defeat Ganon.' % \ - (w.treasure_hunt_required, w.treasure_hunt_total) + (triforce_pieces_required, w.treasure_hunt_total) tt['kakariko_tavern_fisherman'] = TavernMan_texts[local_random.randint(0, len(TavernMan_texts) - 1)] From 13bc121c27228691d6e8a90952e918d69f04425d Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Sat, 1 Jun 2024 14:43:11 +0200 Subject: [PATCH 26/27] Webhost: Sphere Tracker (#3412) --- WebHostLib/templates/hostRoom.html | 3 +- WebHostLib/templates/multispheretracker.html | 72 ++++++++++++++++++++ WebHostLib/tracker.py | 25 +++++++ 3 files changed, 99 insertions(+), 1 deletion(-) create mode 100644 WebHostLib/templates/multispheretracker.html diff --git a/WebHostLib/templates/hostRoom.html b/WebHostLib/templates/hostRoom.html index 2981c41452f0..2bbfe4ad0169 100644 --- a/WebHostLib/templates/hostRoom.html +++ b/WebHostLib/templates/hostRoom.html @@ -24,7 +24,8 @@
{% endif %} {% if room.tracker %} - This room has a Multiworld Tracker enabled. + This room has a Multiworld Tracker + and a Sphere Tracker enabled.
{% endif %} The server for this room will be paused after {{ room.timeout//60//60 }} hours of inactivity. diff --git a/WebHostLib/templates/multispheretracker.html b/WebHostLib/templates/multispheretracker.html new file mode 100644 index 000000000000..a86697498396 --- /dev/null +++ b/WebHostLib/templates/multispheretracker.html @@ -0,0 +1,72 @@ +{% extends "tablepage.html" %} +{% block head %} + {{ super() }} + Multiworld Sphere Tracker + + +{% endblock %} + +{% block body %} + {% include "header/dirtHeader.html" %} + +
+
+ + +
+ {% if tracker_data.get_spheres() %} + This tracker lists already found locations by their logical access sphere. + It ignores items that cannot be sent + and will therefore differ from the sphere numbers in the spoiler playthrough. + This tracker will automatically update itself periodically. + {% else %} + This Multiworld has no Sphere data, likely due to being too old, cannot display data. + {% endif %} +
+
+ +
+ {%- for team, players in tracker_data.get_all_players().items() %} +
+ + + + + {#- Mimicking hint table header for familiarity. #} + + + + + + + + + {%- for sphere in tracker_data.get_spheres() %} + {%- set current_sphere = loop.index %} + {%- for player, sphere_location_ids in sphere.items() %} + {%- set checked_locations = tracker_data.get_player_checked_locations(team, player) %} + {%- set finder_game = tracker_data.get_player_game(team, player) %} + {%- set player_location_data = tracker_data.get_player_locations(team, player) %} + {%- for location_id in sphere_location_ids.intersection(checked_locations) %} + + {%- set item_id, receiver, item_flags = player_location_data[location_id] %} + {%- set receiver_game = tracker_data.get_player_game(team, receiver) %} + + + + + + + + {%- endfor %} + + {%- endfor %} + {%- endfor %} + +
SphereFinderReceiverItemLocationGame
{{ current_sphere }}{{ tracker_data.get_player_name(team, player) }}{{ tracker_data.get_player_name(team, receiver) }}{{ tracker_data.item_id_to_name[receiver_game][item_id] }}{{ tracker_data.location_id_to_name[finder_game][location_id] }}{{ finder_game }}
+
+ + {%- endfor -%} +
+
+{% endblock %} diff --git a/WebHostLib/tracker.py b/WebHostLib/tracker.py index fd233da131e7..71ee9c7fcafe 100644 --- a/WebHostLib/tracker.py +++ b/WebHostLib/tracker.py @@ -291,6 +291,11 @@ def get_room_videos(self) -> Dict[TeamPlayer, Tuple[str, str]]: return video_feeds + @_cache_results + def get_spheres(self) -> List[List[int]]: + """ each sphere is { player: { location_id, ... } } """ + return self._multidata.get("spheres", []) + @app.route("/tracker///") def get_player_tracker(tracker: UUID, tracked_team: int, tracked_player: int, generic: bool = False) -> str: @@ -414,6 +419,26 @@ def render_generic_multiworld_tracker(tracker_data: TrackerData, enabled_tracker ) +def render_generic_multiworld_sphere_tracker(tracker_data: TrackerData) -> str: + return render_template( + "multispheretracker.html", + room=tracker_data.room, + tracker_data=tracker_data, + ) + + +@app.route("/sphere_tracker/") +@cache.memoize(timeout=TRACKER_CACHE_TIMEOUT_IN_SECONDS) +def get_multiworld_sphere_tracker(tracker: UUID): + # Room must exist. + room = Room.get(tracker=tracker) + if not room: + abort(404) + + tracker_data = TrackerData(room) + return render_generic_multiworld_sphere_tracker(tracker_data) + + # TODO: This is a temporary solution until a proper Tracker API can be implemented for tracker templates and data to # live in their respective world folders. From da33d1576abe56811d173350b85ab402704adf5f Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Sat, 1 Jun 2024 17:07:58 +0200 Subject: [PATCH 27/27] WebHost: update trackers only if they're visible. (#3407) --- WebHostLib/static/assets/trackerCommon.js | 85 ++++++++++-------- WebHostLib/templates/multitracker.html | 2 +- WebHostLib/tracker.py | 100 +++++++++++++++------- 3 files changed, 120 insertions(+), 67 deletions(-) diff --git a/WebHostLib/static/assets/trackerCommon.js b/WebHostLib/static/assets/trackerCommon.js index b8e089ece5d3..6324837b2816 100644 --- a/WebHostLib/static/assets/trackerCommon.js +++ b/WebHostLib/static/assets/trackerCommon.js @@ -27,7 +27,7 @@ const adjustTableHeight = () => { * @returns {string} */ const secondsToHours = (seconds) => { - let hours = Math.floor(seconds / 3600); + let hours = Math.floor(seconds / 3600); let minutes = Math.floor((seconds - (hours * 3600)) / 60).toString().padStart(2, '0'); return `${hours}:${minutes}`; }; @@ -38,18 +38,18 @@ window.addEventListener('load', () => { info: false, dom: "t", stateSave: true, - stateSaveCallback: function(settings, data) { + stateSaveCallback: function (settings, data) { delete data.search; localStorage.setItem(`DataTables_${settings.sInstance}_/tracker`, JSON.stringify(data)); }, - stateLoadCallback: function(settings) { + stateLoadCallback: function (settings) { return JSON.parse(localStorage.getItem(`DataTables_${settings.sInstance}_/tracker`)); }, - footerCallback: function(tfoot, data, start, end, display) { + footerCallback: function (tfoot, data, start, end, display) { if (tfoot) { const activityData = this.api().column('lastActivity:name').data().toArray().filter(x => !isNaN(x)); Array.from(tfoot?.children).find(td => td.classList.contains('last-activity')).innerText = - (activityData.length) ? secondsToHours(Math.min(...activityData)) : 'None'; + (activityData.length) ? secondsToHours(Math.min(...activityData)) : 'None'; } }, columnDefs: [ @@ -123,49 +123,64 @@ window.addEventListener('load', () => { event.preventDefault(); } }); - const tracker = document.getElementById('tracker-wrapper').getAttribute('data-tracker'); - const target_second = document.getElementById('tracker-wrapper').getAttribute('data-second') + 3; + const target_second = parseInt(document.getElementById('tracker-wrapper').getAttribute('data-second')) + 3; + console.log("Target second of refresh: " + target_second); - function getSleepTimeSeconds(){ + function getSleepTimeSeconds() { // -40 % 60 is -40, which is absolutely wrong and should burn var sleepSeconds = (((target_second - new Date().getSeconds()) % 60) + 60) % 60; return sleepSeconds || 60; } + let update_on_view = false; const update = () => { - const target = $("
"); - console.log("Updating Tracker..."); - target.load(location.href, function (response, status) { - if (status === "success") { - target.find(".table").each(function (i, new_table) { - const new_trs = $(new_table).find("tbody>tr"); - const footer_tr = $(new_table).find("tfoot>tr"); - const old_table = tables.eq(i); - const topscroll = $(old_table.settings()[0].nScrollBody).scrollTop(); - const leftscroll = $(old_table.settings()[0].nScrollBody).scrollLeft(); - old_table.clear(); - if (footer_tr.length) { - $(old_table.table).find("tfoot").html(footer_tr); - } - old_table.rows.add(new_trs); - old_table.draw(); - $(old_table.settings()[0].nScrollBody).scrollTop(topscroll); - $(old_table.settings()[0].nScrollBody).scrollLeft(leftscroll); - }); - $("#multi-stream-link").replaceWith(target.find("#multi-stream-link")); - } else { - console.log("Failed to connect to Server, in order to update Table Data."); - console.log(response); - } - }) - setTimeout(update, getSleepTimeSeconds()*1000); + if (document.hidden) { + console.log("Document reporting as not visible, not updating Tracker..."); + update_on_view = true; + } else { + update_on_view = false; + const target = $("
"); + console.log("Updating Tracker..."); + target.load(location.href, function (response, status) { + if (status === "success") { + target.find(".table").each(function (i, new_table) { + const new_trs = $(new_table).find("tbody>tr"); + const footer_tr = $(new_table).find("tfoot>tr"); + const old_table = tables.eq(i); + const topscroll = $(old_table.settings()[0].nScrollBody).scrollTop(); + const leftscroll = $(old_table.settings()[0].nScrollBody).scrollLeft(); + old_table.clear(); + if (footer_tr.length) { + $(old_table.table).find("tfoot").html(footer_tr); + } + old_table.rows.add(new_trs); + old_table.draw(); + $(old_table.settings()[0].nScrollBody).scrollTop(topscroll); + $(old_table.settings()[0].nScrollBody).scrollLeft(leftscroll); + }); + $("#multi-stream-link").replaceWith(target.find("#multi-stream-link")); + } else { + console.log("Failed to connect to Server, in order to update Table Data."); + console.log(response); + } + }) + } + updater = setTimeout(update, getSleepTimeSeconds() * 1000); } - setTimeout(update, getSleepTimeSeconds()*1000); + let updater = setTimeout(update, getSleepTimeSeconds() * 1000); window.addEventListener('resize', () => { adjustTableHeight(); tables.draw(); }); + window.addEventListener('visibilitychange', () => { + if (!document.hidden && update_on_view) { + console.log("Page became visible, tracker should be refreshed."); + clearTimeout(updater); + update(); + } + }); + adjustTableHeight(); }); diff --git a/WebHostLib/templates/multitracker.html b/WebHostLib/templates/multitracker.html index b16d4714ec6a..1b371b1229e5 100644 --- a/WebHostLib/templates/multitracker.html +++ b/WebHostLib/templates/multitracker.html @@ -10,7 +10,7 @@ {% include "header/dirtHeader.html" %} {% include "multitrackerNavigation.html" %} -
+
diff --git a/WebHostLib/tracker.py b/WebHostLib/tracker.py index 71ee9c7fcafe..36ebaacbcb0b 100644 --- a/WebHostLib/tracker.py +++ b/WebHostLib/tracker.py @@ -3,8 +3,9 @@ from dataclasses import dataclass from typing import Any, Callable, Dict, List, Optional, Set, Tuple, NamedTuple, Counter from uuid import UUID +from email.utils import parsedate_to_datetime -from flask import render_template +from flask import render_template, make_response, Response, request from werkzeug.exceptions import abort from MultiServer import Context, get_saving_second @@ -297,56 +298,92 @@ def get_spheres(self) -> List[List[int]]: return self._multidata.get("spheres", []) +def _process_if_request_valid(incoming_request, room: Optional[Room]) -> Optional[Response]: + if not room: + abort(404) + + if_modified = incoming_request.headers.get("If-Modified-Since", None) + if if_modified: + if_modified = parsedate_to_datetime(if_modified) + # if_modified has less precision than last_activity, so we bring them to same precision + if if_modified >= room.last_activity.replace(microsecond=0): + return make_response("", 304) + + @app.route("/tracker///") -def get_player_tracker(tracker: UUID, tracked_team: int, tracked_player: int, generic: bool = False) -> str: +def get_player_tracker(tracker: UUID, tracked_team: int, tracked_player: int, generic: bool = False) -> Response: key = f"{tracker}_{tracked_team}_{tracked_player}_{generic}" - tracker_page = cache.get(key) - if tracker_page: - return tracker_page + response: Optional[Response] = cache.get(key) + if response: + return response + + # Room must exist. + room = Room.get(tracker=tracker) + + response = _process_if_request_valid(request, room) + if response: + return response + + timeout, last_modified, tracker_page = get_timeout_and_player_tracker(room, tracked_team, tracked_player, generic) + response = make_response(tracker_page) + response.last_modified = last_modified + cache.set(key, response, timeout) + return response + + +def get_timeout_and_player_tracker(room: Room, tracked_team: int, tracked_player: int, generic: bool)\ + -> Tuple[int, datetime.datetime, str]: + tracker_data = TrackerData(room) + + # Load and render the game-specific player tracker, or fallback to generic tracker if none exists. + game_specific_tracker = _player_trackers.get(tracker_data.get_player_game(tracked_team, tracked_player), None) + if game_specific_tracker and not generic: + tracker = game_specific_tracker(tracker_data, tracked_team, tracked_player) + else: + tracker = render_generic_tracker(tracker_data, tracked_team, tracked_player) - timeout, tracker_page = get_timeout_and_tracker(tracker, tracked_team, tracked_player, generic) - cache.set(key, tracker_page, timeout) - return tracker_page + return ((tracker_data.get_room_saving_second() - datetime.datetime.now().second) + % TRACKER_CACHE_TIMEOUT_IN_SECONDS or TRACKER_CACHE_TIMEOUT_IN_SECONDS, room.last_activity, tracker) @app.route("/generic_tracker///") -def get_generic_game_tracker(tracker: UUID, tracked_team: int, tracked_player: int) -> str: +def get_generic_game_tracker(tracker: UUID, tracked_team: int, tracked_player: int) -> Response: return get_player_tracker(tracker, tracked_team, tracked_player, True) @app.route("/tracker/", defaults={"game": "Generic"}) @app.route("/tracker//") -@cache.memoize(timeout=TRACKER_CACHE_TIMEOUT_IN_SECONDS) -def get_multiworld_tracker(tracker: UUID, game: str): +def get_multiworld_tracker(tracker: UUID, game: str) -> Response: + key = f"{tracker}_{game}" + response: Optional[Response] = cache.get(key) + if response: + return response + # Room must exist. room = Room.get(tracker=tracker) - if not room: - abort(404) - tracker_data = TrackerData(room) - enabled_trackers = list(get_enabled_multiworld_trackers(room).keys()) - if game not in _multiworld_trackers: - return render_generic_multiworld_tracker(tracker_data, enabled_trackers) + response = _process_if_request_valid(request, room) + if response: + return response - return _multiworld_trackers[game](tracker_data, enabled_trackers) + timeout, last_modified, tracker_page = get_timeout_and_multiworld_tracker(room, game) + response = make_response(tracker_page) + response.last_modified = last_modified + cache.set(key, response, timeout) + return response -def get_timeout_and_tracker(tracker: UUID, tracked_team: int, tracked_player: int, generic: bool) -> Tuple[int, str]: - # Room must exist. - room = Room.get(tracker=tracker) - if not room: - abort(404) - +def get_timeout_and_multiworld_tracker(room: Room, game: str)\ + -> Tuple[int, datetime.datetime, str]: tracker_data = TrackerData(room) - - # Load and render the game-specific player tracker, or fallback to generic tracker if none exists. - game_specific_tracker = _player_trackers.get(tracker_data.get_player_game(tracked_team, tracked_player), None) - if game_specific_tracker and not generic: - tracker = game_specific_tracker(tracker_data, tracked_team, tracked_player) + enabled_trackers = list(get_enabled_multiworld_trackers(room).keys()) + if game in _multiworld_trackers: + tracker = _multiworld_trackers[game](tracker_data, enabled_trackers) else: - tracker = render_generic_tracker(tracker_data, tracked_team, tracked_player) + tracker = render_generic_multiworld_tracker(tracker_data, enabled_trackers) - return (tracker_data.get_room_saving_second() - datetime.datetime.now().second) % 60 or 60, tracker + return ((tracker_data.get_room_saving_second() - datetime.datetime.now().second) + % TRACKER_CACHE_TIMEOUT_IN_SECONDS or TRACKER_CACHE_TIMEOUT_IN_SECONDS, room.last_activity, tracker) def get_enabled_multiworld_trackers(room: Room) -> Dict[str, Callable]: @@ -416,6 +453,7 @@ def render_generic_multiworld_tracker(tracker_data: TrackerData, enabled_tracker videos=tracker_data.get_room_videos(), item_id_to_name=tracker_data.item_id_to_name, location_id_to_name=tracker_data.location_id_to_name, + saving_second=tracker_data.get_room_saving_second(), )